merge main 'main' into branch

This commit is contained in:
Sylvia Crowe 2024-12-19 13:35:05 -08:00
commit 44ff4e9071
71 changed files with 1662 additions and 570 deletions

View File

@ -20,6 +20,9 @@
"[less]": { "[less]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": { "[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },

View File

@ -14,12 +14,23 @@
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fwavetermdev%2Fwaveterm.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fwavetermdev%2Fwaveterm?ref=badge_shield) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fwavetermdev%2Fwaveterm.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fwavetermdev%2Fwaveterm?ref=badge_shield)
[![waveterm](https://snapcraft.io/waveterm/trending.svg?name=0)](https://snapcraft.io/waveterm) [![waveterm](https://snapcraft.io/waveterm/trending.svg?name=0)](https://snapcraft.io/waveterm)
Wave is an open-source terminal that can launch graphical widgets, controlled and integrated directly with the CLI. It includes a base terminal, directory browser, file previews (images, media, markdown), a graphical editor (for code/text files), a web browser, and integrated AI chat. Wave is an open-source terminal that combines traditional terminal features with graphical capabilities like file previews, web browsing, and AI assistance. It runs on MacOS, Linux, and Windows.
Wave isn't just another terminal emulator; it's a rethink on how terminals are built. For too long there has been a disconnect between the CLI and the web. If you want fast, keyboard-accessible, easy-to-write applications, you use the CLI, but if you want graphical interfaces, native widgets, copy/paste, scrolling, variable font sizes, then you'd have to turn to the web. Wave's goal is to bridge that gap. Modern development involves constantly switching between terminals and browsers - checking documentation, previewing files, monitoring systems, and using AI tools. Wave brings these graphical tools directly into the terminal, letting you control them from the command line. This means you can stay in your terminal workflow while still having access to the visual interfaces you need.
![WaveTerm Screenshot](./assets/wave-screenshot.webp) ![WaveTerm Screenshot](./assets/wave-screenshot.webp)
## Key Features
- Flexible drag & drop interface to organize terminal blocks, editors, web browsers, and AI assistants
- Built-in editor for seamlessly editing remote files with syntax highlighting and modern editor features
- Rich file preview system for remote files (markdown, images, video, PDFs, CSVs, directories)
- Integrated AI chat with support for multiple models (OpenAI, Claude, Azure, Perplexity, Ollama)
- Command Blocks for isolating and monitoring individual commands with auto-close options
- One-click remote connections with full terminal and file system access
- Rich customization including tab themes, terminal styles, and background images
- Powerful `wsh` command system for managing your workspace from the CLI and sharing data between terminal sessions
## Installation ## Installation
Wave Terminal works on macOS, Linux, and Windows. Wave Terminal works on macOS, Linux, and Windows.
@ -30,12 +41,18 @@ You can also install Wave Terminal directly from: [www.waveterm.dev/download](ht
### Minimum requirements ### Minimum requirements
Wave Terminal and WSH run on the following platforms: Wave Terminal runs on the following platforms:
- macOS 11 or later (arm64, x64) - macOS 11 or later (arm64, x64)
- Windows 10 1809 or later (x64) - Windows 10 1809 or later (x64)
- Linux based on glibc-2.28 or later (Debian 10, RHEL 8, Ubuntu 20.04, etc.) (arm64, x64) - Linux based on glibc-2.28 or later (Debian 10, RHEL 8, Ubuntu 20.04, etc.) (arm64, x64)
The WSH helper runs on the following platforms:
- macOS 11 or later (arm64, x64)
- Windows 10 or later (arm64, x64)
- Linux Kernel 2.6.32 or later (x64), Linux Kernel 3.1 or later (arm64)
## Links ## Links
- Homepage — https://www.waveterm.dev - Homepage — https://www.waveterm.dev
@ -43,6 +60,7 @@ Wave Terminal and WSH run on the following platforms:
- Documentation — https://docs.waveterm.dev - Documentation — https://docs.waveterm.dev
- Legacy Documentation — https://legacydocs.waveterm.dev - Legacy Documentation — https://legacydocs.waveterm.dev
- Blog — https://blog.waveterm.dev - Blog — https://blog.waveterm.dev
- X — https://x.com/wavetermdev
- Discord Community — https://discord.gg/XfvZ334gwU - Discord Community — https://discord.gg/XfvZ334gwU
## Building from Source ## Building from Source

View File

@ -96,7 +96,7 @@ tasks:
cmds: cmds:
- cmd: '{{.RMRF}} "make"' - cmd: '{{.RMRF}} "make"'
ignore_error: true ignore_error: true
- yarn build:prod && yarn electron-builder -c electron-builder.config.cjs -p never - yarn build:prod && yarn electron-builder -c electron-builder.config.cjs -p never {{.CLI_ARGS}}
deps: deps:
- yarn - yarn
- docsite:build:embedded - docsite:build:embedded

View File

@ -0,0 +1,135 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd
import (
"bytes"
"fmt"
"io"
"os"
"github.com/spf13/cobra"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
)
var wavepathCmd = &cobra.Command{
Use: "wavepath {config|data|log}",
Short: "Get paths to various waveterm files and directories",
RunE: wavepathRun,
PreRunE: preRunSetupRpcClient,
}
func init() {
wavepathCmd.Flags().BoolP("open", "o", false, "Open the path in a new block")
wavepathCmd.Flags().BoolP("open-external", "O", false, "Open the path in the default external application")
wavepathCmd.Flags().BoolP("tail", "t", false, "Tail the last 100 lines of the log")
rootCmd.AddCommand(wavepathCmd)
}
func wavepathRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
sendActivity("wavepath", rtnErr == nil)
}()
if len(args) == 0 {
OutputHelpMessage(cmd)
return fmt.Errorf("no arguments. wsh wavepath requires a type argument (config, data, or log)")
}
if len(args) > 1 {
OutputHelpMessage(cmd)
return fmt.Errorf("too many arguments. wsh wavepath requires exactly one argument")
}
pathType := args[0]
if pathType != "config" && pathType != "data" && pathType != "log" {
OutputHelpMessage(cmd)
return fmt.Errorf("invalid path type %q. must be one of: config, data, log", pathType)
}
tail, _ := cmd.Flags().GetBool("tail")
if tail && pathType != "log" {
return fmt.Errorf("--tail can only be used with the log path type")
}
open, _ := cmd.Flags().GetBool("open")
openExternal, _ := cmd.Flags().GetBool("open-external")
path, err := wshclient.PathCommand(RpcClient, wshrpc.PathCommandData{
PathType: pathType,
Open: open,
OpenExternal: openExternal,
}, nil)
if err != nil {
return fmt.Errorf("getting path: %w", err)
}
if tail && pathType == "log" {
err = tailLogFile(path)
if err != nil {
return fmt.Errorf("tailing log file: %w", err)
}
return nil
}
WriteStdout("%s\n", path)
return nil
}
func tailLogFile(path string) error {
file, err := os.Open(path)
if err != nil {
return fmt.Errorf("opening log file: %w", err)
}
defer file.Close()
// Get file size
stat, err := file.Stat()
if err != nil {
return fmt.Errorf("getting file stats: %w", err)
}
// Read last 16KB or whole file if smaller
readSize := int64(16 * 1024)
var offset int64
if stat.Size() > readSize {
offset = stat.Size() - readSize
}
_, err = file.Seek(offset, 0)
if err != nil {
return fmt.Errorf("seeking file: %w", err)
}
buf := make([]byte, readSize)
n, err := file.Read(buf)
if err != nil && err != io.EOF {
return fmt.Errorf("reading file: %w", err)
}
buf = buf[:n]
// Skip partial line at start if we're not at beginning of file
if offset > 0 {
idx := bytes.IndexByte(buf, '\n')
if idx >= 0 {
buf = buf[idx+1:]
}
}
// Split into lines
lines := bytes.Split(buf, []byte{'\n'})
// Take last 100 lines if we have more
startIdx := 0
if len(lines) > 100 {
startIdx = len(lines) - 100
}
// Print lines
for _, line := range lines[startIdx:] {
WriteStdout("%s\n", string(line))
}
return nil
}

View File

@ -25,6 +25,8 @@ wsh editconfig
| Key Name | Type | Function | | Key Name | Type | Function |
| ------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| app:globalhotkey | string | A systemwide keybinding to open your most recent wave window. This is a set of key names separated by `:`. For more info, see [Customizable Systemwide Global Hotkey](#customizable-systemwide-global-hotkey) |
| app:dismissarchitecturewarning | bool | Disable warnings on app start when you are using a non-native architecture for Wave. For more info, see [Why does Wave warn me about ARM64 translation when it launches?](./faq#why-does-wave-warn-me-about-arm64-translation-when-it-launches). |
| ai:preset | string | the default AI preset to use | | ai:preset | string | the default AI preset to use |
| ai:baseurl | string | Set the AI Base Url (must be OpenAI compatible) | | ai:baseurl | string | Set the AI Base Url (must be OpenAI compatible) |
| ai:apitoken | string | your AI api token | | ai:apitoken | string | your AI api token |
@ -40,12 +42,16 @@ wsh editconfig
| term:fontfamily | string | font family to use for terminal block | | term:fontfamily | string | font family to use for terminal block |
| term:disablewebgl | bool | set to false to disable WebGL acceleration in terminal | | term:disablewebgl | bool | set to false to disable WebGL acceleration in terminal |
| term:localshellpath | string | set to override the default shell path for local terminals | | term:localshellpath | string | set to override the default shell path for local terminals |
| term:localshellopts | string[] | set to pass additional parameters to the term:localshellpath | | term:localshellopts | string[] | set to pass additional parameters to the term:localshellpath (example: `["-NoLogo"]` for PowerShell will remove the copyright notice) |
| term:copyonselect | bool | set to false to disable terminal copy-on-select | | term:copyonselect | bool | set to false to disable terminal copy-on-select |
| term:scrollback | int | size of terminal scrollback buffer, max is 10000 | | term:scrollback | int | size of terminal scrollback buffer, max is 10000 |
| term:theme | string | preset name of terminal theme to apply by default (default is "default-dark") |
| term:transparency | float64 | set the background transparency of terminal theme (default 0.5, 0 = not transparent, 1.0 = fully transparent) |
| editor:minimapenabled | bool | set to false to disable editor minimap | | editor:minimapenabled | bool | set to false to disable editor minimap |
| editor:stickyscrollenabled | bool | enables monaco editor's stickyScroll feature (pinning headers of current context, e.g. class names, method names, etc.), defaults to false | | editor:stickyscrollenabled | bool | enables monaco editor's stickyScroll feature (pinning headers of current context, e.g. class names, method names, etc.), defaults to false |
| editor:wordwrap | bool | set to true to enable word wrapping in the editor (defaults to false) | | editor:wordwrap | bool | set to true to enable word wrapping in the editor (defaults to false) |
| markdown:fontsize | float64 | font size for the normal text when rendering markdown in preview. headers are scaled up from this size, (default 14px) |
| markdown:fixedfontsize | float64 | font size for the code blocks when rendering markdown in preview (default is 12px) |
| web:openlinksinternally | bool | set to false to open web links in external browser | | web:openlinksinternally | bool | set to false to open web links in external browser |
| web:defaulturl | string | default web page to open in the web widget when no url is provided (homepage) | | web:defaulturl | string | default web page to open in the web widget when no url is provided (homepage) |
| web:defaultsearch | string | search template for web searches. e.g. `https://www.google.com/search?q={query}`. "\{query}" gets replaced by search term | | web:defaultsearch | string | search template for web searches. e.g. `https://www.google.com/search?q={query}`. "\{query}" gets replaced by search term |
@ -188,3 +194,52 @@ wsh editconfig termthemes.json
| background | CSS color | | | background color (default when no color code is applied), must have alpha channel (#rrggbbaa) if you want the terminal to be transparent | | background | CSS color | | | background color (default when no color code is applied), must have alpha channel (#rrggbbaa) if you want the terminal to be transparent |
| cursorAccent | CSS color | | | color for cursor | | cursorAccent | CSS color | | | color for cursor |
| selectionBackground | CSS color | | | background color for selected text | | selectionBackground | CSS color | | | background color for selected text |
### Customizable Systemwide Global Hotkey
Wave allows settings a custom global hotkey to open your most recent window from anywhere in your computer. This has the name `"app:globalhotkey"` in the `settings.json` file and takes the form of a series of key names separated by the `:` character.
#### Examples
As a practical example, suppose you want a value of `F5` as your global hotkey. Then you can simply set the value of `"app:globalhotkey"` to `"F5"` and reboot Wave to make that your global hotkey.
As a less practical example, suppose you use the combination of the keys `Ctrl`, `Option`, and `e`. Then the value for this keybinding would be `"Ctrl:Option:e"`.
#### Allowed Key Names
We support the following key names:
- `Ctrl`
- `Cmd`
- `Shift`
- `Alt`
- `Option`
- `Meta`
- `Super`
- Digits (non-numpad) represented by `c{Digit0}` through `c{Digit9}`
- Letters `a` though `z`
- F keys `F1` through `F20`
- Soft keys `Soft1` through `Soft4`. These are essentially the same as `F21` through `F24`.
- Space represented as either `Space` or a literal space &nbsp;<code>&nbsp;</code>
- `Enter` (This is labeled as return on Mac)
- `Tab`
- `CapsLock`
- `NumLock`
- `Backspace` (This is labeled as delete on Mac)
- `Delete`
- `Insert`
- The arrow keys `ArrowUp`, `ArrowDown`, `ArrowLeft`, and `ArrowRight`
- `Home`
- `End`
- `PageUp`
- `PageDown`
- `Esc`
- Volume controls `AudioVolumeUp`, `AudioVolumeDown`, `AudioVolumeMute`
- Media controls `MediaTrackNext`, `MediaTrackPrevious`, `MediaPlayPause`, and `MediaStop`
- `PrintScreen`
- Numpad keys represented by `c{Numpad0}` through `c{Numpad9}`
- The numpad decimal represented by `Decimal`
- The numpad plus/add represented by `Add`
- The numpad minus/subtract represented by `Subtract`
- The numpad star/multiply represented by `Multiply`
- The numpad slash/divide represented by `Divide`

View File

@ -76,6 +76,72 @@ In addition to the regular ssh config file, wave also has its own config file to
| term:theme | This string can be used to specify a terminal theme for blocks using this connection. The block metadata takes priority over this setting. It defaults to null which means the global setting will be used instead. | | term:theme | This string can be used to specify a terminal theme for blocks using this connection. The block metadata takes priority over this setting. It defaults to null which means the global setting will be used instead. |
| ssh:identityfile | A list of strings containing the paths to identity files that will be used. If a `wsh ssh` command using the `-i` flag is successful, the identity file will automatically be added here. | | ssh:identityfile | A list of strings containing the paths to identity files that will be used. If a `wsh ssh` command using the `-i` flag is successful, the identity file will automatically be added here. |
### Example Internal Configurations
Here are a couple examples of things you can do using the internal configuration file `connections.json`:
#### Hiding a Connection
Suppose you have a connection named `github.com` in your `~/.ssh/config` file that shows up as `git@github.com` in the connections dropdown. While it does belong in the config file for authentication reasons, it makes no sense to be in the dropdown since it doesn't involve connecting to a remote environment. In that case, you can hide it as in the example below:
```json
{
<... other connections go here ...>,
"git@github.com" : {
"display:hidden": true
},
<... other connections go here ...>
}
```
#### Moving a Connection
Suppose you have a connection named `rarelyused` that shows up as `myusername@rarelyused:9999` in the connections dropdown. Since it's so rarely used, you would prefer to move it later in the list. In that case, you can move it as in the example below:
```json
{
<... other connections go here ...>,
"myusername@rarelyused:9999" : {
"display:order": 100
},
<... other connections go here ...>
}
```
#### Theming a Connection
Suppose you have a connection named `myhost` that shows up as `myusername@myhost` in the connections dropdown. You use this connection a lot, but you keep getting it mixed up with your local connections. In this case, you can use the internal configuration file to style it differently. For example:
```json
{
<... other connections go here ...>,
"myusername@myhost" : {
"term:theme": "warmyellow",
"term:fontsize": 16,
"term:fontfamily": "menlo"
},
<... other connections go here ...>
}
```
This style, font size, and font family will then only apply to the widgets that are using this connection.
### Disabling Wsh for a Connection
While Wave provides an option disable `wsh` when first connecting to a remote, there are cases where you may wish to disable it afterward. The easiest way to do this is by editing the `connections.json` file. Suppose the connection shows up in the dropdown as `root@wshless`. Then you can disable it manually with the following line:
```json
{
<... other connections go here ...>,
"root@wshless" : {
"conn:enablewsh": false,
},
<... other connections go here ...>
}
```
Note that this same line gets added to your `connections.json` file automatically when you choose to disable `wsh` in gui when initially connecting.
## Managing Connections with the CLI ## Managing Connections with the CLI
The `wsh` command gives some commands specifically for interacting with the connections. You can view these [here](/wsh-reference#conn). The `wsh` command gives some commands specifically for interacting with the connections. You can view these [here](/wsh-reference#conn).

View File

@ -109,9 +109,71 @@ The `WidgetConfigType` takes the usual options common to all widgets. The `MetaT
| "term:localshellpath" | (optional) Sets the shell used for running your widget command. Only works locally. If left blank, wave will determine your system default instead. | | "term:localshellpath" | (optional) Sets the shell used for running your widget command. Only works locally. If left blank, wave will determine your system default instead. |
| "term:localshellopts" | (optional) Sets the shell options meant to be used with `"term:localshellpath"`. This is useful if you are using a nonstandard shell and need to provide a specific option that we do not cover. Only works locally. Defaults to an empty string. | | "term:localshellopts" | (optional) Sets the shell options meant to be used with `"term:localshellpath"`. This is useful if you are using a nonstandard shell and need to provide a specific option that we do not cover. Only works locally. Defaults to an empty string. |
## Example Terminal Widgets ## Example Shell Widgets
Here are a few simple widgets to serve as examples. If you have multiple shells installed on your machine, there may be times when you want to use a non-default shell. For cases like this, it is easy to create a widget for each.
Suppose you want a widget to launch a `fish` shell. Once you have `fish` installed on your system, you can define a widget as
```json
{
<... other widgets go here ...>,
"fish" : {
"icon": "fish",
"color": "#4abc39",
"label": "fish",
"blockdef": {
"meta": {
"view": "term",
"controller": "shell",
"term:localshellpath": "/usr/local/bin/fish",
"term:localshellopts": "-i -l"
}
}
},
<... other widgets go here ...>
}
```
This adds an icon to the widget bar that you can press to launch a terminal running the `fish` shell.
![The example fish widget](./img/widget-example-fish.webp)
:::info
It is possible that `fish` is not in your path. If this is true, using `"fish"` as the value of `"term:localshellpath"` will not work. In these cases, you will need to provide a direct path to it. This is often somewhere like `"/usr/local/bin/fish"`, but it may be different on your system.
:::
If you want to do the same for something like Powershell Core, or `pwsh`, you can define the widget as
```json
{
<... other widgets go here ...>,
"pwsh" : {
"icon": "rectangle-terminal",
"color": "#2671be",
"label": "pwsh",
"blockdef": {
"meta": {
"view": "term",
"controller": "shell",
"term:localshellpath": "pwsh"
}
}
},
<... other widgets go here ...>
}
```
This adds an icon to the widget bar that you can press to launch a terminal running the `pwsh` shell.
![The example pwsh widget](./img/widget-example-pwsh.webp)
:::info
It is possible that `pwsh` is not in your path. If this is true, using `"pwsh"` as the value of `"term:localshellpath"` will not work. In these cases, you will need to provide a direct path to it. This could be somewhere like `"/usr/local/bin/pwsh"` on a Unix system or <code>"C:\\Program&nbsp;Files\\PowerShell\\7\\pwsh.exe"</code> on
Windows. but it may be different on your system. Also note that both `pwsh.exe` and `pwsh` work on Windows, but only `pwsh` works on Unix systems.
:::
## Example Cmd Widgets
Here are a few simple cmd widgets to serve as examples.
Suppose I want a widget that will run speedtest-go when opened. Then, I can define a widget as Suppose I want a widget that will run speedtest-go when opened. Then, I can define a widget as
@ -142,6 +204,7 @@ Using `"cmd"` for the `"controller"` is the simplest way to accomplish this. `"c
Now suppose I wanted to run a TUI app, for instance, `dua`. Well, it turns out that you can more or less do the same thing: Now suppose I wanted to run a TUI app, for instance, `dua`. Well, it turns out that you can more or less do the same thing:
```json ```json
{
<... other widgets go here ...>, <... other widgets go here ...>,
"dua" : { "dua" : {
"icon": "brands@linux", "icon": "brands@linux",
@ -155,6 +218,7 @@ Now suppose I wanted to run a TUI app, for instance, `dua`. Well, it turns out t
} }
}, },
<... other widgets go here ...> <... other widgets go here ...>
}
``` ```
This adds an icon to the widget bar that you can press to launch a terminal running the `dua` command. This adds an icon to the widget bar that you can press to launch a terminal running the `dua` command.
@ -196,6 +260,7 @@ The `WidgetConfigType` takes the usual options common to all widgets. The `MetaT
Say you want a widget that automatically starts at YouTube and will use YouTube as the home page. This can be done using: Say you want a widget that automatically starts at YouTube and will use YouTube as the home page. This can be done using:
```json ```json
{
<... other widgets go here ...>, <... other widgets go here ...>,
"youtube" : { "youtube" : {
"icon": "brands@youtube", "icon": "brands@youtube",
@ -209,6 +274,7 @@ Say you want a widget that automatically starts at YouTube and will use YouTube
} }
}, },
<... other widgets go here ...> <... other widgets go here ...>
}
``` ```
This adds an icon to the widget bar that you can press to launch a web widget on the youtube homepage. This adds an icon to the widget bar that you can press to launch a web widget on the youtube homepage.
@ -217,6 +283,7 @@ This adds an icon to the widget bar that you can press to launch a web widget on
Alternatively, say you want a web widget that opens to github as if it were a bookmark, but will use google as its home page after that. This can easily be done with: Alternatively, say you want a web widget that opens to github as if it were a bookmark, but will use google as its home page after that. This can easily be done with:
```json ```json
{
<... other widgets go here ...>, <... other widgets go here ...>,
"github" : { "github" : {
"icon": "brands@github", "icon": "brands@github",
@ -230,6 +297,7 @@ Alternatively, say you want a web widget that opens to github as if it were a bo
} }
}, },
<... other widgets go here ...> <... other widgets go here ...>
}
``` ```
This adds an icon to the widget bar that you can press to launch a web widget on the github homepage. This adds an icon to the widget bar that you can press to launch a web widget on the github homepage.
@ -270,6 +338,7 @@ The `WidgetConfigType` takes the usual options common to all widgets. The `MetaT
Suppose you have a build process that lasts 3 minutes and you'd like to be able to see the entire build on the sysinfo graph. Also, you would really like to view both the cpu and memory since both are impacted by this process. In that case, you can set up a widget as follows: Suppose you have a build process that lasts 3 minutes and you'd like to be able to see the entire build on the sysinfo graph. Also, you would really like to view both the cpu and memory since both are impacted by this process. In that case, you can set up a widget as follows:
```json ```json
{
<... other widgets go here ...>, <... other widgets go here ...>,
"3min-info" : { "3min-info" : {
"icon": "circle-3", "icon": "circle-3",
@ -283,6 +352,7 @@ Suppose you have a build process that lasts 3 minutes and you'd like to be able
} }
}, },
<... other widgets go here ...> <... other widgets go here ...>
}
``` ```
This adds an icon to the widget bar that you can press to launch the CPU and Memory plots by default with 180 seconds of data. This adds an icon to the widget bar that you can press to launch the CPU and Memory plots by default with 180 seconds of data.
@ -291,6 +361,7 @@ This adds an icon to the widget bar that you can press to launch the CPU and Mem
Now, suppose you are fine with the default 100 points (and 100 seconds) but would like to show all of the CPU data when launched. In that case, you can write: Now, suppose you are fine with the default 100 points (and 100 seconds) but would like to show all of the CPU data when launched. In that case, you can write:
```json ```json
{
<... other widgets go here ...>, <... other widgets go here ...>,
"all-cpu" : { "all-cpu" : {
"icon": "chart-scatter", "icon": "chart-scatter",
@ -303,6 +374,7 @@ Now, suppose you are fine with the default 100 points (and 100 seconds) but woul
} }
}, },
<... other widgets go here ...> <... other widgets go here ...>
}
``` ```
This adds an icon to the widget bar that you can press to launch All CPU plots by default. This adds an icon to the widget bar that you can press to launch All CPU plots by default.

View File

@ -28,5 +28,32 @@ the location of the Git Bash "bash.exe" binary. By default it is located at "C:\
Just remember in JSON, backslashes need to be escaped. So add this to your [settings.json](./config) file: Just remember in JSON, backslashes need to be escaped. So add this to your [settings.json](./config) file:
```json ```json
"term:localshellpath": "C:\\Program Files\\Git\\bin\\bash.exe" "term:localshellpath": "C:\\Program Files\\Git\\bin\\bash.exe"
```
### Can I use WSH outside of Wave?
`wsh` is an internal CLI for extending control over Wave to the command line, you can learn more about it [here](./wsh). To prevent misuse by other applications, `wsh` requires an access token provided by Wave to work and will not function outside of the app.
## Why does Wave warn me about ARM64 translation when it launches?
macOS and Windows both have compatibility layers that allow x64 applications to run on ARM computers. This helps more apps run on these systems while developers work to add native ARM support to their applications. However, it comes with significant performance tradeoffs.
To get the best experience using Wave, it is recommended that you uninstall Wave and reinstall the version that is natively compiled for your computer. You can find the right version by consulting our [Installation Instructions](./gettingstarted#installation).
You can disable this warning by setting `app:dismissarchitecturewarning=true` in [your configurations](./config.mdx).
## How do I join the beta builds of Wave?
Wave publishes to two channels, `latest` and `beta`. If you've installed the app for macOS, Windows, or Linux via DEB or RPM, you can set the following configurations in your `settings.json` (see [Configuration](./config) for more info):
```json
"autoupdate:enabled": true,
"autoupdate:channel": "beta"
```
If you've installed via Snap, you can use the following command:
```sh
sudo snap install waveterm --classic --beta
``` ```

View File

@ -14,6 +14,35 @@ Wave Terminal is a modern terminal that includes graphical capabilities like web
<PlatformProvider> <PlatformProvider>
<PlatformSelectorButton /> <PlatformSelectorButton />
### Platform requirements
<PlatformItem platforms={["mac"]}>
- Supported architectures: Apple Silicon, x64
- Supported OS version: macOS 11 Big Sur or later
</PlatformItem>
<PlatformItem platforms={["windows"]}>
- Supported architectures: x64
- Supported OS version: Windows 10 1809 or later, Windows 11
:::note
ARM64 is planned, but is currently blocked by upstream dependencies (see [Windows ARM Support](https://github.com/wavetermdev/waveterm/issues/928)).
:::
</PlatformItem>
<PlatformItem platforms={["linux"]}>
- Supported architectures: x64, ARM64
- Supported OS version: must have glibc-2.28 or later (Debian >=10, RHEL >=8, Ubuntu >=20.04, etc.)
</PlatformItem>
### Package managers ### Package managers
<PlatformItem platforms={["mac"]}> <PlatformItem platforms={["mac"]}>

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

View File

@ -71,4 +71,16 @@ replace "Cmd" with "Alt" (note that "Ctrl" is "Ctrl" on both Mac, Windows, and L
| ---------------- | ------------- | | ---------------- | ------------- |
| <Kbd k="Cmd:l"/> | Clear AI Chat | | <Kbd k="Cmd:l"/> | Clear AI Chat |
## Terminal Keybindings
| Key | Function |
| ----------------------- | -------------- |
| <Kbd k="Ctrl:Shift:c"/> | Copy |
| <Kbd k="Ctrl:Shift:v"/> | Paste |
| <Kbd k="Cmd:k"/> | Clear Terminal |
## Customizeable Systemwide Global Hotkey
Wave allows setting a custom global hotkey to focus your most recent window from anywhere in your computer. For more information on this, see [the config docs](./config#customizable-systemwide-global-hotkey).
</PlatformProvider> </PlatformProvider>

View File

@ -693,4 +693,52 @@ wsh setvar -b client MYVAR=value
Variables set with these commands persist across sessions and can be used to store configuration values, secrets, or any other string data that needs to be accessible across blocks or tabs. Variables set with these commands persist across sessions and can be used to store configuration values, secrets, or any other string data that needs to be accessible across blocks or tabs.
## wavepath
The `wavepath` command lets you get the paths to various Wave Terminal directories and files, including configuration, data storage, and logs.
```bash
wsh wavepath {config|data|log}
```
This command returns the full path to the requested Wave Terminal system directory or file. It's useful for accessing Wave's configuration files, data storage, or checking logs.
Flags:
- `-o, --open` - open the path in a new block
- `-O, --open-external` - open the path in the default external application
- `-t, --tail` - show the last ~100 lines of the log file (only valid for log path)
Examples:
```bash
# Get path to config directory
wsh wavepath config
# Get path to data directory
wsh wavepath data
# Get path to log file
wsh wavepath log
# Open log file in a new block
wsh wavepath -o log
# Open config directory in system file explorer
wsh wavepath -O config
# View recent log entries
wsh wavepath -t log
```
The command will show you the full path to:
- `config` - Where Wave Terminal stores its configuration files
- `data` - Where Wave Terminal stores its persistent data
- `log` - The main Wave Terminal log file
:::tip
Use the `-t` flag with the log path to quickly view recent log entries without having to open the full file. This is particularly useful for troubleshooting.
:::
</PlatformProvider> </PlatformProvider>

View File

@ -53,7 +53,7 @@
"remark-preset-lint-consistent": "^6.0.0", "remark-preset-lint-consistent": "^6.0.0",
"remark-preset-lint-recommended": "^7.0.0", "remark-preset-lint-recommended": "^7.0.0",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"typescript-eslint": "^8.17.0" "typescript-eslint": "^8.18.0"
}, },
"resolutions": { "resolutions": {
"path-to-regexp@npm:2.2.1": "^3", "path-to-regexp@npm:2.2.1": "^3",

View File

@ -166,3 +166,90 @@ export function ensureBoundsAreVisible(bounds: electron.Rectangle): electron.Rec
} }
return bounds; return bounds;
} }
export function waveKeyToElectronKey(waveKey: string): string {
const waveParts = waveKey.split(":");
const electronParts: Array<string> = waveParts.map((part: string) => {
const digitRegexpMatch = new RegExp("^c{Digit([0-9])}$").exec(part);
const numpadRegexpMatch = new RegExp("^c{Numpad([0-9])}$").exec(part);
const lowercaseCharMatch = new RegExp("^([a-z])$").exec(part);
if (part == "ArrowUp") {
return "Up";
}
if (part == "ArrowDown") {
return "Down";
}
if (part == "ArrowLeft") {
return "Left";
}
if (part == "ArrowRight") {
return "Right";
}
if (part == "Soft1") {
return "F21";
}
if (part == "Soft2") {
return "F22";
}
if (part == "Soft3") {
return "F23";
}
if (part == "Soft4") {
return "F24";
}
if (part == " ") {
return "Space";
}
if (part == "CapsLock") {
return "Capslock";
}
if (part == "NumLock") {
return "Numlock";
}
if (part == "ScrollLock") {
return "Scrolllock";
}
if (part == "AudioVolumeUp") {
return "VolumeUp";
}
if (part == "AudioVolumeDown") {
return "VolumeDown";
}
if (part == "AudioVolumeMute") {
return "VolumeMute";
}
if (part == "MediaTrackNext") {
return "MediaNextTrack";
}
if (part == "MediaTrackPrevious") {
return "MediaPreviousTrack";
}
if (part == "Decimal") {
return "numdec";
}
if (part == "Add") {
return "numadd";
}
if (part == "Subtract") {
return "numsub";
}
if (part == "Multiply") {
return "nummult";
}
if (part == "Divide") {
return "numdiv";
}
if (digitRegexpMatch && digitRegexpMatch.length > 1) {
return digitRegexpMatch[1];
}
if (numpadRegexpMatch && numpadRegexpMatch.length > 1) {
return `num${numpadRegexpMatch[1]}`;
}
if (lowercaseCharMatch && lowercaseCharMatch.length > 1) {
return lowercaseCharMatch[1].toUpperCase();
}
return part;
});
return electronParts.join("+");
}

View File

@ -3,7 +3,7 @@
import { ClientService, FileService, ObjectService, WindowService, WorkspaceService } from "@/app/store/services"; import { ClientService, FileService, ObjectService, WindowService, WorkspaceService } from "@/app/store/services";
import { fireAndForget } from "@/util/util"; import { fireAndForget } from "@/util/util";
import { BaseWindow, BaseWindowConstructorOptions, dialog, ipcMain, screen } from "electron"; import { BaseWindow, BaseWindowConstructorOptions, dialog, globalShortcut, ipcMain, screen } from "electron";
import path from "path"; import path from "path";
import { debounce } from "throttle-debounce"; import { debounce } from "throttle-debounce";
import { import {
@ -14,7 +14,7 @@ import {
setWasInFg, setWasInFg,
} from "./emain-activity"; } from "./emain-activity";
import { getOrCreateWebViewForTab, getWaveTabViewByWebContentsId, WaveTabView } from "./emain-tabview"; import { getOrCreateWebViewForTab, getWaveTabViewByWebContentsId, WaveTabView } from "./emain-tabview";
import { delay, ensureBoundsAreVisible } from "./emain-util"; import { delay, ensureBoundsAreVisible, waveKeyToElectronKey } from "./emain-util";
import { log } from "./log"; import { log } from "./log";
import { getElectronAppBasePath, unamePlatform } from "./platform"; import { getElectronAppBasePath, unamePlatform } from "./platform";
import { updater } from "./updater"; import { updater } from "./updater";
@ -766,3 +766,23 @@ export async function relaunchBrowserWindows() {
win.show(); win.show();
} }
} }
export function registerGlobalHotkey(rawGlobalHotKey: string) {
try {
const electronHotKey = waveKeyToElectronKey(rawGlobalHotKey);
console.log("registering globalhotkey of ", electronHotKey);
globalShortcut.register(electronHotKey, () => {
const selectedWindow = focusedWaveWindow;
const firstWaveWindow = getAllWaveWindows()[0];
if (focusedWaveWindow) {
selectedWindow.focus();
} else if (firstWaveWindow) {
firstWaveWindow.focus();
} else {
fireAndForget(createNewWaveWindow);
}
});
} catch (e) {
console.log("error registering global hotkey: ", e);
}
}

View File

@ -38,6 +38,7 @@ import {
getWaveWindowById, getWaveWindowById,
getWaveWindowByWebContentsId, getWaveWindowByWebContentsId,
getWaveWindowByWorkspaceId, getWaveWindowByWorkspaceId,
registerGlobalHotkey,
relaunchBrowserWindows, relaunchBrowserWindows,
WaveBrowserWindow, WaveBrowserWindow,
} from "./emain-window"; } from "./emain-window";
@ -46,6 +47,7 @@ import { getLaunchSettings } from "./launchsettings";
import { log } from "./log"; import { log } from "./log";
import { makeAppMenu } from "./menu"; import { makeAppMenu } from "./menu";
import { import {
checkIfRunningUnderARM64Translation,
getElectronAppBasePath, getElectronAppBasePath,
getElectronAppUnpackedBasePath, getElectronAppUnpackedBasePath,
getWaveConfigDir, getWaveConfigDir,
@ -587,6 +589,7 @@ async function appMain() {
await electronApp.whenReady(); await electronApp.whenReady();
configureAuthKeyRequestInjection(electron.session.defaultSession); configureAuthKeyRequestInjection(electron.session.defaultSession);
const fullConfig = await services.FileService.GetFullConfig(); const fullConfig = await services.FileService.GetFullConfig();
checkIfRunningUnderARM64Translation(fullConfig);
ensureHotSpareTab(fullConfig); ensureHotSpareTab(fullConfig);
await relaunchBrowserWindows(); await relaunchBrowserWindows();
await initDocsite(); await initDocsite();
@ -610,6 +613,10 @@ async function appMain() {
fireAndForget(createNewWaveWindow); fireAndForget(createNewWaveWindow);
} }
}); });
const rawGlobalHotKey = launchSettings?.["app:globalhotkey"];
if (rawGlobalHotKey) {
registerGlobalHotkey(rawGlobalHotKey);
}
} }
appMain().catch((e) => { appMain().catch((e) => {

View File

@ -1,7 +1,8 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { app, ipcMain } from "electron"; import { fireAndForget } from "@/util/util";
import { app, dialog, ipcMain, shell } from "electron";
import envPaths from "env-paths"; import envPaths from "env-paths";
import { existsSync, mkdirSync } from "fs"; import { existsSync, mkdirSync } from "fs";
import os from "os"; import os from "os";
@ -40,6 +41,32 @@ const WaveConfigHomeVarName = "WAVETERM_CONFIG_HOME";
const WaveDataHomeVarName = "WAVETERM_DATA_HOME"; const WaveDataHomeVarName = "WAVETERM_DATA_HOME";
const WaveHomeVarName = "WAVETERM_HOME"; const WaveHomeVarName = "WAVETERM_HOME";
export function checkIfRunningUnderARM64Translation(fullConfig: FullConfigType) {
if (!fullConfig.settings["app:dismissarchitecturewarning"] && app.runningUnderARM64Translation) {
console.log("Running under ARM64 translation, alerting user");
const dialogOpts: Electron.MessageBoxOptions = {
type: "warning",
buttons: ["Dismiss", "Learn More"],
title: "Wave has detected a performance issue",
message: `Wave is running in ARM64 translation mode which may impact performance.\n\nRecommendation: Download the native ARM64 version from our website for optimal performance.`,
};
const choice = dialog.showMessageBoxSync(null, dialogOpts);
if (choice === 1) {
// Open the documentation URL
console.log("User chose to learn more");
fireAndForget(() =>
shell.openExternal(
"https://docs.waveterm.dev/faq#why-does-wave-warn-me-about-arm64-translation-when-it-launches"
)
);
throw new Error("User redirected to docsite to learn more about ARM64 translation, exiting");
} else {
console.log("User dismissed the dialog");
}
}
}
/** /**
* Gets the path to the old Wave home directory (defaults to `~/.waveterm`). * Gets the path to the old Wave home directory (defaults to `~/.waveterm`).
* @returns The path to the directory if it exists and contains valid data for the current app, otherwise null. * @returns The path to the directory if it exists and contains valid data for the current app, otherwise null.

View File

@ -166,7 +166,7 @@
flex: 1 2 auto; flex: 1 2 auto;
overflow: hidden; overflow: hidden;
padding-right: 4px; padding-right: 4px;
@include mixins.ellipsis() @include mixins.ellipsis();
} }
.connecting-svg { .connecting-svg {
@ -211,12 +211,12 @@
} }
} }
.button { .wave-button {
margin-left: 3px; margin-left: 3px;
} }
// webview specific. for refresh button // webview specific. for refresh button
.iconbutton { .wave-iconbutton {
height: 100%; height: 100%;
width: 27px; width: 27px;
display: flex; display: flex;
@ -226,7 +226,7 @@
} }
.menubutton { .menubutton {
.button { .wave-button {
font-size: 11px; font-size: 11px;
} }
} }
@ -236,7 +236,7 @@
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
.iconbutton { .wave-iconbutton {
display: flex; display: flex;
width: 24px; width: 24px;
padding: 4px 6px; padding: 4px 6px;
@ -268,7 +268,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
.iconbutton { .wave-iconbutton {
opacity: 0.7; opacity: 0.7;
font-size: 45px; font-size: 45px;
margin: -30px 0 0 0; margin: -30px 0 0 0;
@ -373,7 +373,7 @@
} }
} }
.button:last-child { .wave-button:last-child {
margin-top: 1.5px; margin-top: 1.5px;
} }
} }

View File

@ -3,7 +3,7 @@
@use "../mixins.scss"; @use "../mixins.scss";
.button { .wave-button {
// override default button appearance // override default button appearance
border: 1px solid transparent; border: 1px solid transparent;
outline: 1px solid transparent; outline: 1px solid transparent;

View File

@ -32,7 +32,7 @@ const Button = memo(
<Component <Component
ref={btnRef} ref={btnRef}
tabIndex={disabled ? -1 : 0} tabIndex={disabled ? -1 : 0}
className={clsx("button", finalClassName)} className={clsx("wave-button", finalClassName)}
disabled={disabled} disabled={disabled}
{...props} {...props}
> >

View File

@ -1,4 +1,4 @@
.iconbutton { .wave-iconbutton {
display: flex; display: flex;
cursor: pointer; cursor: pointer;
opacity: 0.7; opacity: 0.7;

View File

@ -16,7 +16,7 @@ export const IconButton = memo(
return ( return (
<button <button
ref={ref} ref={ref}
className={clsx("iconbutton", className, decl.className, { className={clsx("wave-iconbutton", className, decl.className, {
disabled: decl.disabled, disabled: decl.disabled,
"no-action": decl.noAction, "no-action": decl.noAction,
})} })}

View File

@ -1,6 +1,12 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { getWebServerEndpoint } from "@/util/endpoints";
import { isBlank, makeConnRoute } from "@/util/util";
import parseSrcSet from "parse-srcset";
export type MarkdownContentBlockType = { export type MarkdownContentBlockType = {
type: string; type: string;
id: string; id: string;
@ -147,3 +153,56 @@ export function transformBlocks(content: string): { content: string; blocks: Map
blocks: blocks, blocks: blocks,
}; };
} }
export const resolveRemoteFile = async (filepath: string, resolveOpts: MarkdownResolveOpts): Promise<string | null> => {
if (!filepath || filepath.startsWith("http://") || filepath.startsWith("https://")) {
return filepath;
}
try {
const route = makeConnRoute(resolveOpts.connName);
const fileInfo = await RpcApi.RemoteFileJoinCommand(TabRpcClient, [resolveOpts.baseDir, filepath], {
route: route,
});
const usp = new URLSearchParams();
usp.set("path", fileInfo.path);
if (!isBlank(resolveOpts.connName)) {
usp.set("connection", resolveOpts.connName);
}
return getWebServerEndpoint() + "/wave/stream-file?" + usp.toString();
} catch (err) {
console.warn("Failed to resolve remote file:", filepath, err);
return null;
}
};
export const resolveSrcSet = async (srcSet: string, resolveOpts: MarkdownResolveOpts): Promise<string> => {
if (!srcSet) return null;
// Parse the srcset
const candidates = parseSrcSet(srcSet);
// Resolve each URL in the array of candidates
const resolvedCandidates = await Promise.all(
candidates.map(async (candidate) => {
const resolvedUrl = await resolveRemoteFile(candidate.url, resolveOpts);
return {
...candidate,
url: resolvedUrl,
};
})
);
// Reconstruct the srcset string
return resolvedCandidates
.map((candidate) => {
let part = candidate.url;
if (candidate.w) part += ` ${candidate.w}w`;
if (candidate.h) part += ` ${candidate.h}h`;
if (candidate.d) part += ` ${candidate.d}x`;
return part;
})
.join(", ");
};

View File

@ -16,21 +16,70 @@
overflow: scroll; overflow: scroll;
line-height: 1.5; line-height: 1.5;
color: var(--main-text-color); color: var(--main-text-color);
font-family: var(--markdown-font); font-family: var(--markdown-font-family);
font-size: 14px; font-size: var(--markdown-font-size);
overflow-wrap: break-word; overflow-wrap: break-word;
&.non-scrollable { &.non-scrollable {
overflow: hidden; overflow: hidden;
} }
.heading { *:last-child {
&:first-of-type { margin-bottom: 0 !important;
}
.heading:not(.heading ~ .heading) {
margin-top: 0 !important; margin-top: 0 !important;
} }
.heading {
color: var(--main-text-color); color: var(--main-text-color);
margin-top: 16px; margin-top: 1.143em;
margin-bottom: 8px; margin-bottom: 0.571em;
font-weight: semibold;
padding-top: 0.429em;
&.is-1 {
border-bottom: 1px solid var(--border-color);
padding-bottom: 0.429em;
font-size: 2em;
}
&.is-2 {
border-bottom: 1px solid var(--border-color);
padding-bottom: 0.429em;
font-size: 1.5em;
}
&.is-3 {
font-size: 1.25em;
}
&.is-4 {
font-size: 1em;
}
&.is-5 {
font-size: 0.875em;
}
&.is-6 {
font-size: 0.85em;
}
}
.paragraph {
margin-top: 0;
margin-bottom: 10px;
}
img {
border-style: none;
max-width: 100%;
box-sizing: content-box;
&[align="right"] {
padding-left: 20px;
}
&[align="left"] {
padding-right: 20px;
}
} }
strong { strong {
@ -44,24 +93,24 @@
ul { ul {
list-style-type: disc; list-style-type: disc;
list-style-position: outside; list-style-position: outside;
margin-left: 16px; margin-left: 1.143em;
} }
ol { ol {
list-style-position: outside; list-style-position: outside;
margin-left: 19px; margin-left: 1.357em;
} }
blockquote { blockquote {
margin: 4px 10px 4px 10px; margin: 0.286em 0.714em;
border-radius: 3px; border-radius: 4px;
background-color: var(--panel-bg-color); background-color: var(--panel-bg-color);
padding: 2px 4px 2px 6px; padding: 0.143em 0.286em 0.143em 0.429em;
} }
pre.codeblock { pre.codeblock {
background-color: var(--panel-bg-color); background-color: var(--panel-bg-color);
margin: 4px 10px; margin: 0.286em 0.714em;
padding: 0.4em 0.7em; padding: 0.4em 0.7em;
border-radius: 4px; border-radius: 4px;
position: relative; position: relative;
@ -83,11 +132,11 @@
right: 0; right: 0;
border-radius: 4px; border-radius: 4px;
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
margin: 2px 2px; margin: 0.143em;
padding: 4px 4px; padding: 0.286em;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
gap: 4px; gap: 0.286em;
} }
&:hover .codeblock-actions { &:hover .codeblock-actions {
@ -98,6 +147,7 @@
code { code {
color: var(--main-text-color); color: var(--main-text-color);
font: var(--fixed-font); font: var(--fixed-font);
font-size: var(--markdown-fixed-font-size);
border-radius: 4px; border-radius: 4px;
} }
@ -105,41 +155,13 @@
outline: 2px solid var(--accent-color); outline: 2px solid var(--accent-color);
} }
.heading {
font-weight: semibold;
padding-top: 6px;
}
.heading.is-1 {
border-bottom: 1px solid var(--border-color);
padding-bottom: 6px;
font-size: 2em;
}
.heading.is-2 {
border-bottom: 1px solid var(--border-color);
padding-bottom: 6px;
font-size: 1.5em;
}
.heading.is-3 {
font-size: 1.25em;
}
.heading.is-4 {
font-size: 1em;
}
.heading.is-5 {
font-size: 0.875em;
}
.heading.is-6 {
font-size: 0.85em;
}
.waveblock { .waveblock {
margin: 16px 0; margin: 1.143em 0;
.wave-block-content { .wave-block-content {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 12px; padding: 0.857em;
background-color: var(--highlight-bg-color); background-color: var(--highlight-bg-color);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 8px; border-radius: 8px;
@ -150,15 +172,15 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 40px; width: 2.857em;
height: 40px; height: 2.857em;
background-color: black; background-color: black;
border-radius: 8px; border-radius: 8px;
margin-right: 12px; margin-right: 0.857em;
} }
.wave-block-icon i { .wave-block-icon i {
font-size: 18px; font-size: 1.125em;
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
@ -168,19 +190,18 @@
} }
.wave-block-filename { .wave-block-filename {
font-size: 14px; font-size: 1em;
font-weight: 500; font-weight: 500;
color: var(--main-text-color); color: var(--main-text-color);
} }
.wave-block-size { .wave-block-size {
font-size: 12px; font-size: 0.857em;
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
} }
} }
// The TOC view should scroll independently of the contents view.
.toc { .toc {
max-width: 40%; max-width: 40%;
height: 100%; height: 100%;
@ -192,21 +213,20 @@
top: 0; top: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 5px; gap: 0.357em;
text-wrap: wrap; text-wrap: wrap;
h4 { h4 {
padding-left: 5px; padding-left: 0.357em;
} }
.toc-item { .toc-item {
cursor: pointer; cursor: pointer;
--indent-factor: 1; --indent-factor: 1;
// The offset in the padding will ensure that when the text in the item wraps, it indents slightly.
// The 5px offset in the padding will ensure that when the text in the item wraps, it indents slightly.
// The indent factor is set in the React code and denotes the depth of the item in the TOC tree. // The indent factor is set in the React code and denotes the depth of the item in the TOC tree.
padding-left: calc((var(--indent-factor) - 1) * 10px + 5px); padding-left: calc((var(--indent-factor) - 1) * 0.714em + 0.357em);
text-indent: -5px; text-indent: -0.357em;
} }
} }
} }

View File

@ -3,11 +3,13 @@
import { CopyButton } from "@/app/element/copybutton"; import { CopyButton } from "@/app/element/copybutton";
import { createContentBlockPlugin } from "@/app/element/markdown-contentblock-plugin"; import { createContentBlockPlugin } from "@/app/element/markdown-contentblock-plugin";
import { MarkdownContentBlockType, transformBlocks } from "@/app/element/markdown-util"; import {
import { RpcApi } from "@/app/store/wshclientapi"; MarkdownContentBlockType,
import { TabRpcClient } from "@/app/store/wshrpcutil"; resolveRemoteFile,
import { getWebServerEndpoint } from "@/util/endpoints"; resolveSrcSet,
import { isBlank, makeConnRoute, useAtomValueSafe } from "@/util/util"; transformBlocks,
} from "@/app/element/markdown-util";
import { boundNumber, useAtomValueSafe } from "@/util/util";
import { clsx } from "clsx"; import { clsx } from "clsx";
import { Atom } from "jotai"; import { Atom } from "jotai";
import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react"; import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
@ -108,8 +110,34 @@ const CodeBlock = ({ children, onClickExecute }: CodeBlockProps) => {
); );
}; };
const MarkdownSource = (props: React.HTMLAttributes<HTMLSourceElement>) => { const MarkdownSource = ({
props,
resolveOpts,
}: {
props: React.HTMLAttributes<HTMLSourceElement> & {
srcSet?: string;
media?: string;
};
resolveOpts: MarkdownResolveOpts;
}) => {
const [resolvedSrcSet, setResolvedSrcSet] = useState<string>(props.srcSet);
const [resolving, setResolving] = useState<boolean>(true);
useEffect(() => {
const resolvePath = async () => {
const resolved = await resolveSrcSet(props.srcSet, resolveOpts);
setResolvedSrcSet(resolved);
setResolving(false);
};
resolvePath();
}, [props.srcSet]);
if (resolving) {
return null; return null;
}
return <source srcSet={resolvedSrcSet} media={props.media} />;
}; };
interface WaveBlockProps { interface WaveBlockProps {
@ -148,16 +176,11 @@ const MarkdownImg = ({
resolveOpts: MarkdownResolveOpts; resolveOpts: MarkdownResolveOpts;
}) => { }) => {
const [resolvedSrc, setResolvedSrc] = useState<string>(props.src); const [resolvedSrc, setResolvedSrc] = useState<string>(props.src);
const [resolvedSrcSet, setResolvedSrcSet] = useState<string>(props.srcSet);
const [resolvedStr, setResolvedStr] = useState<string>(null); const [resolvedStr, setResolvedStr] = useState<string>(null);
const [resolving, setResolving] = useState<boolean>(true); const [resolving, setResolving] = useState<boolean>(true);
useEffect(() => { useEffect(() => {
if (props.src.startsWith("http://") || props.src.startsWith("https://")) {
setResolving(false);
setResolvedSrc(props.src);
setResolvedStr(null);
return;
}
if (props.src.startsWith("data:image/")) { if (props.src.startsWith("data:image/")) {
setResolving(false); setResolving(false);
setResolvedSrc(props.src); setResolvedSrc(props.src);
@ -170,23 +193,20 @@ const MarkdownImg = ({
setResolvedStr(`[img:${props.src}]`); setResolvedStr(`[img:${props.src}]`);
return; return;
} }
const resolveFn = async () => { const resolveFn = async () => {
const route = makeConnRoute(resolveOpts.connName); const [resolvedSrc, resolvedSrcSet] = await Promise.all([
const fileInfo = await RpcApi.RemoteFileJoinCommand(TabRpcClient, [resolveOpts.baseDir, props.src], { resolveRemoteFile(props.src, resolveOpts),
route: route, resolveSrcSet(props.srcSet, resolveOpts),
}); ]);
const usp = new URLSearchParams();
usp.set("path", fileInfo.path); setResolvedSrc(resolvedSrc);
if (!isBlank(resolveOpts.connName)) { setResolvedSrcSet(resolvedSrcSet);
usp.set("connection", resolveOpts.connName);
}
const streamingUrl = getWebServerEndpoint() + "/wave/stream-file?" + usp.toString();
setResolvedSrc(streamingUrl);
setResolvedStr(null); setResolvedStr(null);
setResolving(false); setResolving(false);
}; };
resolveFn(); resolveFn();
}, [props.src]); }, [props.src, props.srcSet]);
if (resolving) { if (resolving) {
return null; return null;
@ -195,7 +215,7 @@ const MarkdownImg = ({
return <span>{resolvedStr}</span>; return <span>{resolvedStr}</span>;
} }
if (resolvedSrc != null) { if (resolvedSrc != null) {
return <img {...props} src={resolvedSrc} />; return <img {...props} src={resolvedSrc} srcSet={resolvedSrcSet} />;
} }
return <span>[img]</span>; return <span>[img]</span>;
}; };
@ -210,6 +230,8 @@ type MarkdownProps = {
resolveOpts?: MarkdownResolveOpts; resolveOpts?: MarkdownResolveOpts;
scrollable?: boolean; scrollable?: boolean;
rehype?: boolean; rehype?: boolean;
fontSizeOverride?: number;
fixedFontSizeOverride?: number;
}; };
const Markdown = ({ const Markdown = ({
@ -219,6 +241,8 @@ const Markdown = ({
style, style,
className, className,
resolveOpts, resolveOpts,
fontSizeOverride,
fixedFontSizeOverride,
scrollable = true, scrollable = true,
rehype = true, rehype = true,
onClickExecute, onClickExecute,
@ -262,7 +286,9 @@ const Markdown = ({
h5: (props: React.HTMLAttributes<HTMLHeadingElement>) => <Heading props={props} hnum={5} />, h5: (props: React.HTMLAttributes<HTMLHeadingElement>) => <Heading props={props} hnum={5} />,
h6: (props: React.HTMLAttributes<HTMLHeadingElement>) => <Heading props={props} hnum={6} />, h6: (props: React.HTMLAttributes<HTMLHeadingElement>) => <Heading props={props} hnum={6} />,
img: (props: React.HTMLAttributes<HTMLImageElement>) => <MarkdownImg props={props} resolveOpts={resolveOpts} />, img: (props: React.HTMLAttributes<HTMLImageElement>) => <MarkdownImg props={props} resolveOpts={resolveOpts} />,
source: (props: React.HTMLAttributes<HTMLSourceElement>) => <MarkdownSource {...props} />, source: (props: React.HTMLAttributes<HTMLSourceElement>) => (
<MarkdownSource props={props} resolveOpts={resolveOpts} />
),
code: Code, code: Code,
pre: (props: React.HTMLAttributes<HTMLPreElement>) => ( pre: (props: React.HTMLAttributes<HTMLPreElement>) => (
<CodeBlock children={props.children} onClickExecute={onClickExecute} /> <CodeBlock children={props.children} onClickExecute={onClickExecute} />
@ -301,12 +327,15 @@ const Markdown = ({
...(defaultSchema.attributes?.span || []), ...(defaultSchema.attributes?.span || []),
// Allow all class names starting with `hljs-`. // Allow all class names starting with `hljs-`.
["className", /^hljs-./], ["className", /^hljs-./],
["srcset"],
["media"],
["type"],
// Alternatively, to allow only certain class names: // Alternatively, to allow only certain class names:
// ['className', 'hljs-number', 'hljs-title', 'hljs-variable'] // ['className', 'hljs-number', 'hljs-title', 'hljs-variable']
], ],
waveblock: [["blockkey"]], waveblock: [["blockkey"]],
}, },
tagNames: [...(defaultSchema.tagNames || []), "span", "waveblock"], tagNames: [...(defaultSchema.tagNames || []), "span", "waveblock", "picture", "source"],
}), }),
() => rehypeSlug({ prefix: idPrefix }), () => rehypeSlug({ prefix: idPrefix }),
]; ];
@ -349,8 +378,15 @@ const Markdown = ({
); );
}; };
const mergedStyle = { ...style };
if (fontSizeOverride != null) {
mergedStyle["--markdown-font-size"] = `${boundNumber(fontSizeOverride, 6, 64)}px`;
}
if (fixedFontSizeOverride != null) {
mergedStyle["--markdown-fixed-font-size"] = `${boundNumber(fixedFontSizeOverride, 6, 64)}px`;
}
return ( return (
<div className={clsx("markdown", className)} style={style}> <div className={clsx("markdown", className)} style={mergedStyle}>
{scrollable ? <ScrollableMarkdown /> : <NonScrollableMarkdown />} {scrollable ? <ScrollableMarkdown /> : <NonScrollableMarkdown />}
{toc && ( {toc && (
<OverlayScrollbarsComponent className="toc" options={{ scrollbars: { autoHide: "leave" } }}> <OverlayScrollbarsComponent className="toc" options={{ scrollbars: { autoHide: "leave" } }}>

View File

@ -2,7 +2,7 @@
overflow: hidden; overflow: hidden;
.menu-anchor { .menu-anchor {
width: 100%; width: 100%;
.button { .wave-button {
width: 100%; width: 100%;
div { div {
max-width: 100%; max-width: 100%;

View File

@ -48,7 +48,7 @@
align-items: flex-start; align-items: flex-start;
gap: 10px; gap: 10px;
.button { .wave-button {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -65,7 +65,7 @@
padding-top: 16px; padding-top: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.1); border-top: 1px solid rgba(255, 255, 255, 0.1);
.button:last-child { .wave-button:last-child {
margin-left: 8px; margin-left: 8px;
} }
} }

View File

@ -230,6 +230,7 @@
justify-content: center; justify-content: center;
button { button {
padding: 8px 20px;
font-size: 14px; font-size: 14px;
} }

View File

@ -202,6 +202,11 @@ class RpcApiType {
return client.wshRpcCall("notify", data, opts); return client.wshRpcCall("notify", data, opts);
} }
// command "path" [call]
PathCommand(client: WshClient, data: PathCommandData, opts?: RpcOpts): Promise<string> {
return client.wshRpcCall("path", data, opts);
}
// command "remotefiledelete" [call] // command "remotefiledelete" [call]
RemoteFileDeleteCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<void> { RemoteFileDeleteCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("remotefiledelete", data, opts); return client.wshRpcCall("remotefiledelete", data, opts);

View File

@ -81,7 +81,7 @@
} }
} }
.button { .wave-button {
position: absolute; position: absolute;
top: 50%; top: 50%;
right: 4px; right: 4px;

View File

@ -1,4 +1,4 @@
.button { .wave-button {
color: black; color: black;
background-color: var(--accent-color); background-color: var(--accent-color);
flex: 0 0 fit-content; flex: 0 0 fit-content;

View File

@ -0,0 +1,70 @@
.workspace-editor {
width: 100%;
.input {
margin: 5px 0 10px;
}
.color-selector {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(15px, 15px)); // Ensures each color circle has a fixed 14px size
grid-gap: 18.5px; // Space between items
justify-content: center;
align-items: center;
margin-top: 5px;
padding-bottom: 15px;
border-bottom: 1px solid var(--modal-border-color);
.color-circle {
width: 15px;
height: 15px;
border-radius: 50%;
cursor: pointer;
position: relative;
// Border offset outward
&:before {
content: "";
position: absolute;
top: -3px;
left: -3px;
right: -3px;
bottom: -3px;
border-radius: 50%;
border: 1px solid transparent;
}
&.selected:before {
border-color: var(--main-text-color); // Highlight for the selected circle
}
}
}
.icon-selector {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16px, 16px)); // Ensures each color circle has a fixed 14px size
grid-column-gap: 17.5px; // Space between items
grid-row-gap: 13px; // Space between items
justify-content: center;
align-items: center;
margin-top: 15px;
.icon-item {
font-size: 15px;
color: oklch(from var(--modal-bg-color) calc(l * 1.5) c h);
cursor: pointer;
transition: color 0.3s ease;
&.selected,
&:hover {
color: var(--main-text-color);
}
}
}
.delete-ws-btn-wrapper {
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
}
}

View File

@ -0,0 +1,125 @@
import { fireAndForget, makeIconClass } from "@/util/util";
import clsx from "clsx";
import { memo, useEffect, useRef, useState } from "react";
import { Button } from "../element/button";
import { Input } from "../element/input";
import { WorkspaceService } from "../store/services";
import "./workspaceeditor.scss";
interface ColorSelectorProps {
colors: string[];
selectedColor?: string;
onSelect: (color: string) => void;
className?: string;
}
const ColorSelector = memo(({ colors, selectedColor, onSelect, className }: ColorSelectorProps) => {
const handleColorClick = (color: string) => {
onSelect(color);
};
return (
<div className={clsx("color-selector", className)}>
{colors.map((color) => (
<div
key={color}
className={clsx("color-circle", { selected: selectedColor === color })}
style={{ backgroundColor: color }}
onClick={() => handleColorClick(color)}
/>
))}
</div>
);
});
interface IconSelectorProps {
icons: string[];
selectedIcon?: string;
onSelect: (icon: string) => void;
className?: string;
}
const IconSelector = memo(({ icons, selectedIcon, onSelect, className }: IconSelectorProps) => {
const handleIconClick = (icon: string) => {
onSelect(icon);
};
return (
<div className={clsx("icon-selector", className)}>
{icons.map((icon) => {
const iconClass = makeIconClass(icon, true);
return (
<i
key={icon}
className={clsx(iconClass, "icon-item", { selected: selectedIcon === icon })}
onClick={() => handleIconClick(icon)}
/>
);
})}
</div>
);
});
interface WorkspaceEditorProps {
title: string;
icon: string;
color: string;
focusInput: boolean;
onTitleChange: (newTitle: string) => void;
onColorChange: (newColor: string) => void;
onIconChange: (newIcon: string) => void;
onDeleteWorkspace: () => void;
}
const WorkspaceEditorComponent = ({
title,
icon,
color,
focusInput,
onTitleChange,
onColorChange,
onIconChange,
onDeleteWorkspace,
}: WorkspaceEditorProps) => {
const inputRef = useRef<HTMLInputElement>(null);
const [colors, setColors] = useState<string[]>([]);
const [icons, setIcons] = useState<string[]>([]);
useEffect(() => {
fireAndForget(async () => {
const colors = await WorkspaceService.GetColors();
const icons = await WorkspaceService.GetIcons();
setColors(colors);
setIcons(icons);
});
}, []);
useEffect(() => {
if (focusInput && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [focusInput]);
return (
<div className="workspace-editor">
<Input
ref={inputRef}
className={clsx("vertical-padding-3", { error: title === "" })}
onChange={onTitleChange}
value={title}
autoFocus
autoSelect
/>
<ColorSelector selectedColor={color} colors={colors} onSelect={onColorChange} />
<IconSelector selectedIcon={icon} icons={icons} onSelect={onIconChange} />
<div className="delete-ws-btn-wrapper">
<Button className="ghost red font-size-12 bold" onClick={onDeleteWorkspace}>
Delete workspace
</Button>
</div>
</div>
);
};
export const WorkspaceEditor = memo(WorkspaceEditorComponent) as typeof WorkspaceEditorComponent;

View File

@ -26,25 +26,6 @@
} }
} }
.icon-left,
.icon-right {
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
.divider {
width: 1px;
height: 20px;
background: rgba(255, 255, 255, 0.08);
}
.scrollable {
max-height: 400px;
width: 100%;
}
.workspace-switcher-content { .workspace-switcher-content {
min-height: auto; min-height: auto;
display: flex; display: flex;
@ -55,6 +36,25 @@
border-radius: 8px; border-radius: 8px;
box-shadow: 0px 8px 24px 0px var(--modal-shadow-color); box-shadow: 0px 8px 24px 0px var(--modal-shadow-color);
.icon-left,
.icon-right {
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
.divider {
width: 1px;
height: 20px;
background: rgba(255, 255, 255, 0.08);
}
.scrollable {
max-height: 400px;
width: 100%;
}
.title { .title {
font-size: 12px; font-size: 12px;
line-height: 19px; line-height: 19px;
@ -100,17 +100,17 @@
gap: 5px; gap: 5px;
} }
.iconbutton.edit { .wave-iconbutton.edit {
visibility: hidden; visibility: hidden;
} }
.iconbutton.window { .wave-iconbutton.window {
cursor: default; cursor: default;
opacity: 1 !important; opacity: 1 !important;
} }
} }
&:hover .iconbutton.edit { &:hover .wave-iconbutton.edit {
visibility: visible; visibility: visible;
} }
@ -144,83 +144,6 @@
padding: 0; padding: 0;
} }
.workspace-editor {
width: 100%;
.input {
margin: 5px 0 10px;
}
.color-selector {
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(15px, 15px)
); // Ensures each color circle has a fixed 14px size
grid-gap: 18.5px; // Space between items
justify-content: center;
align-items: center;
margin-top: 5px;
padding-bottom: 15px;
border-bottom: 1px solid var(--modal-border-color);
.color-circle {
width: 15px;
height: 15px;
border-radius: 50%;
cursor: pointer;
position: relative;
// Border offset outward
&:before {
content: "";
position: absolute;
top: -3px;
left: -3px;
right: -3px;
bottom: -3px;
border-radius: 50%;
border: 1px solid transparent;
}
&.selected:before {
border-color: var(--main-text-color); // Highlight for the selected circle
}
}
}
.icon-selector {
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(16px, 16px)
); // Ensures each color circle has a fixed 14px size
grid-column-gap: 17.5px; // Space between items
grid-row-gap: 13px; // Space between items
justify-content: center;
align-items: center;
margin-top: 15px;
.icon-item {
font-size: 15px;
color: oklch(from var(--modal-bg-color) calc(l * 1.5) c h);
cursor: pointer;
transition: color 0.3s ease;
&.selected,
&:hover {
color: var(--main-text-color);
}
}
}
.delete-ws-btn-wrapper {
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
}
}
.actions { .actions {
width: 100%; width: 100%;
padding: 3px 0; padding: 3px 0;

View File

@ -1,7 +1,6 @@
// Copyright 2024, Command Line // Copyright 2024, Command Line
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { Button } from "@/element/button";
import { import {
ExpandableMenu, ExpandableMenu,
ExpandableMenuItem, ExpandableMenuItem,
@ -10,139 +9,22 @@ import {
ExpandableMenuItemLeftElement, ExpandableMenuItemLeftElement,
ExpandableMenuItemRightElement, ExpandableMenuItemRightElement,
} from "@/element/expandablemenu"; } from "@/element/expandablemenu";
import { Input } from "@/element/input";
import { Popover, PopoverButton, PopoverContent } from "@/element/popover"; import { Popover, PopoverButton, PopoverContent } from "@/element/popover";
import { fireAndForget, makeIconClass, useAtomValueSafe } from "@/util/util"; import { fireAndForget, makeIconClass, useAtomValueSafe } from "@/util/util";
import clsx from "clsx"; import clsx from "clsx";
import { atom, PrimitiveAtom, useAtom, useAtomValue, useSetAtom } from "jotai"; import { atom, PrimitiveAtom, useAtom, useAtomValue, useSetAtom } from "jotai";
import { splitAtom } from "jotai/utils"; import { splitAtom } from "jotai/utils";
import { OverlayScrollbarsComponent } from "overlayscrollbars-react"; import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
import { CSSProperties, forwardRef, memo, useCallback, useEffect, useRef, useState } from "react"; import { CSSProperties, forwardRef, useCallback, useEffect } from "react";
import WorkspaceSVG from "../asset/workspace.svg"; import WorkspaceSVG from "../asset/workspace.svg";
import { IconButton } from "../element/iconbutton"; import { IconButton } from "../element/iconbutton";
import { atoms, getApi } from "../store/global"; import { atoms, getApi } from "../store/global";
import { WorkspaceService } from "../store/services"; import { WorkspaceService } from "../store/services";
import { getObjectValue, makeORef, setObjectValue } from "../store/wos"; import { getObjectValue, makeORef } from "../store/wos";
import { waveEventSubscribe } from "../store/wps";
import { WorkspaceEditor } from "./workspaceeditor";
import "./workspaceswitcher.scss"; import "./workspaceswitcher.scss";
interface ColorSelectorProps {
colors: string[];
selectedColor?: string;
onSelect: (color: string) => void;
className?: string;
}
const ColorSelector = memo(({ colors, selectedColor, onSelect, className }: ColorSelectorProps) => {
const handleColorClick = (color: string) => {
onSelect(color);
};
return (
<div className={clsx("color-selector", className)}>
{colors.map((color) => (
<div
key={color}
className={clsx("color-circle", { selected: selectedColor === color })}
style={{ backgroundColor: color }}
onClick={() => handleColorClick(color)}
/>
))}
</div>
);
});
interface IconSelectorProps {
icons: string[];
selectedIcon?: string;
onSelect: (icon: string) => void;
className?: string;
}
const IconSelector = memo(({ icons, selectedIcon, onSelect, className }: IconSelectorProps) => {
const handleIconClick = (icon: string) => {
onSelect(icon);
};
return (
<div className={clsx("icon-selector", className)}>
{icons.map((icon) => {
const iconClass = makeIconClass(icon, true);
return (
<i
key={icon}
className={clsx(iconClass, "icon-item", { selected: selectedIcon === icon })}
onClick={() => handleIconClick(icon)}
/>
);
})}
</div>
);
});
interface WorkspaceEditorProps {
title: string;
icon: string;
color: string;
focusInput: boolean;
onTitleChange: (newTitle: string) => void;
onColorChange: (newColor: string) => void;
onIconChange: (newIcon: string) => void;
onDeleteWorkspace: () => void;
}
const WorkspaceEditor = memo(
({
title,
icon,
color,
focusInput,
onTitleChange,
onColorChange,
onIconChange,
onDeleteWorkspace,
}: WorkspaceEditorProps) => {
const inputRef = useRef<HTMLInputElement>(null);
const [colors, setColors] = useState<string[]>([]);
const [icons, setIcons] = useState<string[]>([]);
useEffect(() => {
fireAndForget(async () => {
const colors = await WorkspaceService.GetColors();
const icons = await WorkspaceService.GetIcons();
setColors(colors);
setIcons(icons);
});
}, []);
useEffect(() => {
if (focusInput && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [focusInput]);
return (
<div className="workspace-editor">
<Input
ref={inputRef}
className={clsx("vertical-padding-3", { error: title === "" })}
onChange={onTitleChange}
value={title}
autoFocus
autoSelect
/>
<ColorSelector selectedColor={color} colors={colors} onSelect={onColorChange} />
<IconSelector selectedIcon={icon} icons={icons} onSelect={onIconChange} />
<div className="delete-ws-btn-wrapper">
<Button className="ghost red font-size-12 bold" onClick={onDeleteWorkspace}>
Delete workspace
</Button>
</div>
</div>
);
}
);
type WorkspaceListEntry = { type WorkspaceListEntry = {
windowId: string; windowId: string;
workspace: Workspace; workspace: Workspace;
@ -175,15 +57,21 @@ const WorkspaceSwitcher = forwardRef<HTMLDivElement>((_, ref) => {
setWorkspaceList(newList); setWorkspaceList(newList);
}, []); }, []);
useEffect(
() =>
waveEventSubscribe({
eventType: "workspace:update",
handler: () => fireAndForget(updateWorkspaceList),
}),
[]
);
useEffect(() => { useEffect(() => {
fireAndForget(updateWorkspaceList); fireAndForget(updateWorkspaceList);
}, []); }, []);
const onDeleteWorkspace = useCallback((workspaceId: string) => { const onDeleteWorkspace = useCallback((workspaceId: string) => {
getApi().deleteWorkspace(workspaceId); getApi().deleteWorkspace(workspaceId);
setTimeout(() => {
fireAndForget(updateWorkspaceList);
}, 10);
}, []); }, []);
const isActiveWorkspaceSaved = !!(activeWorkspace.name && activeWorkspace.icon); const isActiveWorkspaceSaved = !!(activeWorkspace.name && activeWorkspace.icon);
@ -266,9 +154,16 @@ const WorkspaceSwitcherItem = ({
const setWorkspace = useCallback((newWorkspace: Workspace) => { const setWorkspace = useCallback((newWorkspace: Workspace) => {
if (newWorkspace.name != "") { if (newWorkspace.name != "") {
setObjectValue({ ...newWorkspace, otype: "workspace" }, undefined, true); fireAndForget(() =>
WorkspaceService.UpdateWorkspace(
workspace.oid,
newWorkspace.name,
newWorkspace.icon,
newWorkspace.color,
false
)
);
} }
setWorkspaceEntry({ ...workspaceEntry, workspace: newWorkspace });
}, []); }, []);
const isActive = !!workspaceEntry.windowId; const isActive = !!workspaceEntry.windowId;

View File

@ -1,8 +1,6 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// Used for syntax highlighting in markdown
:root { :root {
--main-text-color: #f7f7f7; --main-text-color: #f7f7f7;
--title-font-size: 18px; --title-font-size: 18px;
@ -16,8 +14,10 @@
--accent-color: rgb(88, 193, 66); --accent-color: rgb(88, 193, 66);
--panel-bg-color: rgba(31, 33, 31, 0.5); --panel-bg-color: rgba(31, 33, 31, 0.5);
--highlight-bg-color: rgba(255, 255, 255, 0.2); --highlight-bg-color: rgba(255, 255, 255, 0.2);
--markdown-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, --markdown-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji"; "Apple Color Emoji", "Segoe UI Emoji";
--markdown-font-size: 14px;
--markdown-fixed-font-size: 12px;
--error-color: rgb(229, 77, 46); --error-color: rgb(229, 77, 46);
--warning-color: rgb(224, 185, 86); --warning-color: rgb(224, 185, 86);
--success-color: rgb(78, 154, 6); --success-color: rgb(78, 154, 6);

View File

@ -122,6 +122,7 @@ export function CodeEditor({ blockId, text, language, filename, meta, onChange,
const minimapEnabled = useOverrideConfigAtom(blockId, "editor:minimapenabled") ?? false; const minimapEnabled = useOverrideConfigAtom(blockId, "editor:minimapenabled") ?? false;
const stickyScrollEnabled = useOverrideConfigAtom(blockId, "editor:stickyscrollenabled") ?? false; const stickyScrollEnabled = useOverrideConfigAtom(blockId, "editor:stickyscrollenabled") ?? false;
const wordWrap = useOverrideConfigAtom(blockId, "editor:wordwrap") ?? false; const wordWrap = useOverrideConfigAtom(blockId, "editor:wordwrap") ?? false;
const fontSize = useOverrideConfigAtom(blockId, "editor:fontsize");
const theme = "wave-theme-dark"; const theme = "wave-theme-dark";
React.useEffect(() => { React.useEffect(() => {
@ -150,8 +151,9 @@ export function CodeEditor({ blockId, text, language, filename, meta, onChange,
opts.minimap.enabled = minimapEnabled; opts.minimap.enabled = minimapEnabled;
opts.stickyScroll.enabled = stickyScrollEnabled; opts.stickyScroll.enabled = stickyScrollEnabled;
opts.wordWrap = wordWrap ? "on" : "off"; opts.wordWrap = wordWrap ? "on" : "off";
opts.fontSize = fontSize;
return opts; return opts;
}, [minimapEnabled, stickyScrollEnabled, wordWrap]); }, [minimapEnabled, stickyScrollEnabled, wordWrap, fontSize]);
return ( return (
<div className="code-editor-wrapper"> <div className="code-editor-wrapper">

View File

@ -783,6 +783,8 @@ function makePreviewModel(blockId: string, nodeModel: BlockNodeModel): PreviewMo
function MarkdownPreview({ model }: SpecializedViewProps) { function MarkdownPreview({ model }: SpecializedViewProps) {
const connName = useAtomValue(model.connection); const connName = useAtomValue(model.connection);
const fileInfo = useAtomValue(model.statFile); const fileInfo = useAtomValue(model.statFile);
const fontSizeOverride = useAtomValue(getOverrideConfigAtom(model.blockId, "markdown:fontsize"));
const fixedFontSizeOverride = useAtomValue(getOverrideConfigAtom(model.blockId, "markdown:fixedfontsize"));
const resolveOpts: MarkdownResolveOpts = useMemo<MarkdownResolveOpts>(() => { const resolveOpts: MarkdownResolveOpts = useMemo<MarkdownResolveOpts>(() => {
return { return {
connName: connName, connName: connName,
@ -791,7 +793,13 @@ function MarkdownPreview({ model }: SpecializedViewProps) {
}, [connName, fileInfo.dir]); }, [connName, fileInfo.dir]);
return ( return (
<div className="view-preview view-preview-markdown"> <div className="view-preview view-preview-markdown">
<Markdown textAtom={model.fileContent} showTocAtom={model.markdownShowToc} resolveOpts={resolveOpts} /> <Markdown
textAtom={model.fileContent}
showTocAtom={model.markdownShowToc}
resolveOpts={resolveOpts}
fontSizeOverride={fontSizeOverride}
fixedFontSizeOverride={fixedFontSizeOverride}
/>
</div> </div>
); );
} }

View File

@ -24,6 +24,7 @@ import {
} from "@/store/global"; } from "@/store/global";
import * as services from "@/store/services"; import * as services from "@/store/services";
import * as keyutil from "@/util/keyutil"; import * as keyutil from "@/util/keyutil";
import { boundNumber } from "@/util/util";
import clsx from "clsx"; import clsx from "clsx";
import debug from "debug"; import debug from "debug";
import * as jotai from "jotai"; import * as jotai from "jotai";
@ -62,6 +63,7 @@ class TermViewModel implements ViewModel {
vdomToolbarTarget: jotai.PrimitiveAtom<VDomTargetToolbar>; vdomToolbarTarget: jotai.PrimitiveAtom<VDomTargetToolbar>;
fontSizeAtom: jotai.Atom<number>; fontSizeAtom: jotai.Atom<number>;
termThemeNameAtom: jotai.Atom<string>; termThemeNameAtom: jotai.Atom<string>;
termTransparencyAtom: jotai.Atom<number>;
noPadding: jotai.PrimitiveAtom<boolean>; noPadding: jotai.PrimitiveAtom<boolean>;
endIconButtons: jotai.Atom<IconButtonDecl[]>; endIconButtons: jotai.Atom<IconButtonDecl[]>;
shellProcFullStatus: jotai.PrimitiveAtom<BlockControllerRuntimeStatus>; shellProcFullStatus: jotai.PrimitiveAtom<BlockControllerRuntimeStatus>;
@ -203,10 +205,17 @@ class TermViewModel implements ViewModel {
return get(getOverrideConfigAtom(this.blockId, "term:theme")) ?? DefaultTermTheme; return get(getOverrideConfigAtom(this.blockId, "term:theme")) ?? DefaultTermTheme;
}); });
}); });
this.termTransparencyAtom = useBlockAtom(blockId, "termtransparencyatom", () => {
return jotai.atom<number>((get) => {
let value = get(getOverrideConfigAtom(this.blockId, "term:transparency")) ?? 0.5;
return boundNumber(value, 0, 1);
});
});
this.blockBg = jotai.atom((get) => { this.blockBg = jotai.atom((get) => {
const fullConfig = get(atoms.fullConfigAtom); const fullConfig = get(atoms.fullConfigAtom);
const themeName = get(this.termThemeNameAtom); const themeName = get(this.termThemeNameAtom);
const [_, bgcolor] = computeTheme(fullConfig, themeName); const termTransparency = get(this.termTransparencyAtom);
const [_, bgcolor] = computeTheme(fullConfig, themeName, termTransparency);
if (bgcolor != null) { if (bgcolor != null) {
return { bg: bgcolor }; return { bg: bgcolor };
} }
@ -407,6 +416,11 @@ class TermViewModel implements ViewModel {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
return false; return false;
} else if (keyutil.checkKeyPressed(waveEvent, "Cmd:k")) {
event.preventDefault();
event.stopPropagation();
this.termRef.current?.terminal?.clear();
return false;
} }
const shellProcStatus = globalStore.get(this.shellProcStatus); const shellProcStatus = globalStore.get(this.shellProcStatus);
if ((shellProcStatus == "done" || shellProcStatus == "init") && keyutil.checkKeyPressed(waveEvent, "Enter")) { if ((shellProcStatus == "done" || shellProcStatus == "init") && keyutil.checkKeyPressed(waveEvent, "Enter")) {
@ -453,6 +467,7 @@ class TermViewModel implements ViewModel {
const termThemeKeys = Object.keys(termThemes); const termThemeKeys = Object.keys(termThemes);
const curThemeName = globalStore.get(getBlockMetaKeyAtom(this.blockId, "term:theme")); const curThemeName = globalStore.get(getBlockMetaKeyAtom(this.blockId, "term:theme"));
const defaultFontSize = globalStore.get(getSettingsKeyAtom("term:fontsize")) ?? 12; const defaultFontSize = globalStore.get(getSettingsKeyAtom("term:fontsize")) ?? 12;
const transparencyMeta = globalStore.get(getBlockMetaKeyAtom(this.blockId, "term:transparency"));
const blockData = globalStore.get(this.blockAtom); const blockData = globalStore.get(this.blockAtom);
const overrideFontSize = blockData?.meta?.["term:fontsize"]; const overrideFontSize = blockData?.meta?.["term:fontsize"];
@ -474,6 +489,41 @@ class TermViewModel implements ViewModel {
checked: curThemeName == null, checked: curThemeName == null,
click: () => this.setTerminalTheme(null), click: () => this.setTerminalTheme(null),
}); });
const transparencySubMenu: ContextMenuItem[] = [];
transparencySubMenu.push({
label: "Default",
type: "checkbox",
checked: transparencyMeta == null,
click: () => {
RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("block", this.blockId),
meta: { "term:transparency": null },
});
},
});
transparencySubMenu.push({
label: "Transparent Background",
type: "checkbox",
checked: transparencyMeta == 0.5,
click: () => {
RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("block", this.blockId),
meta: { "term:transparency": 0.5 },
});
},
});
transparencySubMenu.push({
label: "No Transparency",
type: "checkbox",
checked: transparencyMeta == 0,
click: () => {
RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("block", this.blockId),
meta: { "term:transparency": 0 },
});
},
});
const fontSizeSubMenu: ContextMenuItem[] = [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18].map( const fontSizeSubMenu: ContextMenuItem[] = [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18].map(
(fontSize: number) => { (fontSize: number) => {
return { return {
@ -508,6 +558,10 @@ class TermViewModel implements ViewModel {
label: "Font Size", label: "Font Size",
submenu: fontSizeSubMenu, submenu: fontSizeSubMenu,
}); });
fullMenu.push({
label: "Transparency",
submenu: transparencySubMenu,
});
fullMenu.push({ type: "separator" }); fullMenu.push({ type: "separator" });
fullMenu.push({ fullMenu.push({
label: "Force Restart Controller", label: "Force Restart Controller",
@ -734,7 +788,8 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
React.useEffect(() => { React.useEffect(() => {
const fullConfig = globalStore.get(atoms.fullConfigAtom); const fullConfig = globalStore.get(atoms.fullConfigAtom);
const termThemeName = globalStore.get(model.termThemeNameAtom); const termThemeName = globalStore.get(model.termThemeNameAtom);
const [termTheme, _] = computeTheme(fullConfig, termThemeName); const termTransparency = globalStore.get(model.termTransparencyAtom);
const [termTheme, _] = computeTheme(fullConfig, termThemeName, termTransparency);
let termScrollback = 1000; let termScrollback = 1000;
if (termSettings?.["term:scrollback"]) { if (termSettings?.["term:scrollback"]) {
termScrollback = Math.floor(termSettings["term:scrollback"]); termScrollback = Math.floor(termSettings["term:scrollback"]);

View File

@ -17,7 +17,8 @@ interface TermThemeProps {
const TermThemeUpdater = ({ blockId, model, termRef }: TermThemeProps) => { const TermThemeUpdater = ({ blockId, model, termRef }: TermThemeProps) => {
const fullConfig = useAtomValue(atoms.fullConfigAtom); const fullConfig = useAtomValue(atoms.fullConfigAtom);
const blockTermTheme = useAtomValue(model.termThemeNameAtom); const blockTermTheme = useAtomValue(model.termThemeNameAtom);
const [theme, _] = computeTheme(fullConfig, blockTermTheme); const transparency = useAtomValue(model.termTransparencyAtom);
const [theme, _] = computeTheme(fullConfig, blockTermTheme, transparency);
useEffect(() => { useEffect(() => {
if (termRef.current?.terminal) { if (termRef.current?.terminal) {
termRef.current.terminal.options.theme = theme; termRef.current.terminal.options.theme = theme;

View File

@ -2,14 +2,32 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
export const DefaultTermTheme = "default-dark"; export const DefaultTermTheme = "default-dark";
import { colord } from "colord";
// returns (theme, bgcolor) function applyTransparencyToColor(hexColor: string, transparency: number): string {
function computeTheme(fullConfig: FullConfigType, themeName: string): [TermThemeType, string] { const alpha = 1 - transparency; // transparency is already 0-1
return colord(hexColor).alpha(alpha).toHex();
}
// returns (theme, bgcolor, transparency (0 - 1.0))
function computeTheme(
fullConfig: FullConfigType,
themeName: string,
termTransparency: number
): [TermThemeType, string] {
let theme: TermThemeType = fullConfig?.termthemes?.[themeName]; let theme: TermThemeType = fullConfig?.termthemes?.[themeName];
if (theme == null) { if (theme == null) {
theme = fullConfig?.termthemes?.[DefaultTermTheme] || ({} as any); theme = fullConfig?.termthemes?.[DefaultTermTheme] || ({} as any);
} }
const themeCopy = { ...theme }; const themeCopy = { ...theme };
if (termTransparency != null && termTransparency > 0) {
if (themeCopy.background) {
themeCopy.background = applyTransparencyToColor(themeCopy.background, termTransparency);
}
if (themeCopy.selectionBackground) {
themeCopy.selectionBackground = applyTransparencyToColor(themeCopy.selectionBackground, termTransparency);
}
}
let bgcolor = themeCopy.background; let bgcolor = themeCopy.background;
themeCopy.background = "#00000000"; themeCopy.background = "#00000000";
return [themeCopy, bgcolor]; return [themeCopy, bgcolor];

View File

@ -165,6 +165,9 @@ export class TermWrap {
} }
handleTermData(data: string) { handleTermData(data: string) {
if (!this.loaded) {
return;
}
const b64data = util.stringToBase64(data); const b64data = util.stringToBase64(data);
RpcApi.ControllerInputCommand(TabRpcClient, { blockid: this.blockId, inputdata64: b64data }); RpcApi.ControllerInputCommand(TabRpcClient, { blockid: this.blockId, inputdata64: b64data });
} }

View File

@ -131,9 +131,6 @@
outline: none; outline: none;
overflow: auto; overflow: auto;
overflow-wrap: anywhere; overflow-wrap: anywhere;
font-family: var(--termfontfamily);
font-weight: normal;
line-height: var(--termlineheight);
height: 21px; height: 21px;
} }
} }

View File

@ -8,7 +8,7 @@ import { RpcResponseHelper, WshClient } from "@/app/store/wshclient";
import { RpcApi } from "@/app/store/wshclientapi"; import { RpcApi } from "@/app/store/wshclientapi";
import { makeFeBlockRouteId } from "@/app/store/wshrouter"; import { makeFeBlockRouteId } from "@/app/store/wshrouter";
import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil"; import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil";
import { atoms, createBlock, fetchWaveFile, getApi, globalStore, WOS } from "@/store/global"; import { atoms, createBlock, fetchWaveFile, getApi, globalStore, useOverrideConfigAtom, WOS } from "@/store/global";
import { BlockService, ObjectService } from "@/store/services"; import { BlockService, ObjectService } from "@/store/services";
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil"; import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
import { fireAndForget, isBlank, makeIconClass } from "@/util/util"; import { fireAndForget, isBlank, makeIconClass } from "@/util/util";
@ -30,6 +30,7 @@ const slidingWindowSize = 30;
interface ChatItemProps { interface ChatItemProps {
chatItem: ChatMessageType; chatItem: ChatMessageType;
model: WaveAiModel;
} }
function promptToMsg(prompt: OpenAIPromptMessageType): ChatMessageType { function promptToMsg(prompt: OpenAIPromptMessageType): ChatMessageType {
@ -430,11 +431,12 @@ function makeWaveAiViewModel(blockId: string): WaveAiModel {
return waveAiModel; return waveAiModel;
} }
const ChatItem = ({ chatItem }: ChatItemProps) => { const ChatItem = ({ chatItem, model }: ChatItemProps) => {
const { user, text } = chatItem; const { user, text } = chatItem;
const cssVar = "--panel-bg-color"; const cssVar = "--panel-bg-color";
const panelBgColor = getComputedStyle(document.documentElement).getPropertyValue(cssVar).trim(); const panelBgColor = getComputedStyle(document.documentElement).getPropertyValue(cssVar).trim();
const fontSize = useOverrideConfigAtom(model.blockId, "ai:fontsize");
const fixedFontSize = useOverrideConfigAtom(model.blockId, "ai:fixedfontsize");
const renderContent = useMemo(() => { const renderContent = useMemo(() => {
if (user == "error") { if (user == "error") {
return ( return (
@ -445,7 +447,12 @@ const ChatItem = ({ chatItem }: ChatItemProps) => {
</div> </div>
</div> </div>
<div className="chat-msg chat-msg-error"> <div className="chat-msg chat-msg-error">
<Markdown text={text} scrollable={false} /> <Markdown
text={text}
scrollable={false}
fontSizeOverride={fontSize}
fixedFontSizeOverride={fixedFontSize}
/>
</div> </div>
</> </>
); );
@ -459,7 +466,12 @@ const ChatItem = ({ chatItem }: ChatItemProps) => {
</div> </div>
</div> </div>
<div className="chat-msg chat-msg-assistant"> <div className="chat-msg chat-msg-assistant">
<Markdown text={text} scrollable={false} /> <Markdown
text={text}
scrollable={false}
fontSizeOverride={fontSize}
fixedFontSizeOverride={fixedFontSize}
/>
</div> </div>
</> </>
) : ( ) : (
@ -474,11 +486,17 @@ const ChatItem = ({ chatItem }: ChatItemProps) => {
return ( return (
<> <>
<div className="chat-msg chat-msg-user"> <div className="chat-msg chat-msg-user">
<Markdown className="msg-text" text={text} scrollable={false} /> <Markdown
className="msg-text"
text={text}
scrollable={false}
fontSizeOverride={fontSize}
fixedFontSizeOverride={fixedFontSize}
/>
</div> </div>
</> </>
); );
}, [text, user]); }, [text, user, fontSize, fixedFontSize]);
return <div className={"chat-msg-container"}>{renderContent}</div>; return <div className={"chat-msg-container"}>{renderContent}</div>;
}; };
@ -487,10 +505,11 @@ interface ChatWindowProps {
chatWindowRef: React.RefObject<HTMLDivElement>; chatWindowRef: React.RefObject<HTMLDivElement>;
messages: ChatMessageType[]; messages: ChatMessageType[];
msgWidths: Object; msgWidths: Object;
model: WaveAiModel;
} }
const ChatWindow = memo( const ChatWindow = memo(
forwardRef<OverlayScrollbarsComponentRef, ChatWindowProps>(({ chatWindowRef, messages, msgWidths }, ref) => { forwardRef<OverlayScrollbarsComponentRef, ChatWindowProps>(({ chatWindowRef, messages, msgWidths, model }, ref) => {
const [isUserScrolling, setIsUserScrolling] = useState(false); const [isUserScrolling, setIsUserScrolling] = useState(false);
const osRef = useRef<OverlayScrollbarsComponentRef>(null); const osRef = useRef<OverlayScrollbarsComponentRef>(null);
@ -559,7 +578,7 @@ const ChatWindow = memo(
<div ref={chatWindowRef} className="chat-window" style={msgWidths}> <div ref={chatWindowRef} className="chat-window" style={msgWidths}>
<div className="filler"></div> <div className="filler"></div>
{messages.map((chitem, idx) => ( {messages.map((chitem, idx) => (
<ChatItem key={idx} chatItem={chitem} /> <ChatItem key={idx} chatItem={chitem} model={model} />
))} ))}
</div> </div>
</OverlayScrollbarsComponent> </OverlayScrollbarsComponent>
@ -569,7 +588,7 @@ const ChatWindow = memo(
interface ChatInputProps { interface ChatInputProps {
value: string; value: string;
termFontSize: number; baseFontSize: number;
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void; onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void; onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
onMouseDown: (e: React.MouseEvent<HTMLTextAreaElement>) => void; onMouseDown: (e: React.MouseEvent<HTMLTextAreaElement>) => void;
@ -577,7 +596,7 @@ interface ChatInputProps {
} }
const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>( const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>(
({ value, onChange, onKeyDown, onMouseDown, termFontSize, model }, ref) => { ({ value, onChange, onKeyDown, onMouseDown, baseFontSize, model }, ref) => {
const textAreaRef = useRef<HTMLTextAreaElement>(null); const textAreaRef = useRef<HTMLTextAreaElement>(null);
useImperativeHandle(ref, () => textAreaRef.current as HTMLTextAreaElement); useImperativeHandle(ref, () => textAreaRef.current as HTMLTextAreaElement);
@ -594,7 +613,7 @@ const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>(
// Adjust the height of the textarea to fit the text // Adjust the height of the textarea to fit the text
const textAreaMaxLines = 5; const textAreaMaxLines = 5;
const textAreaLineHeight = termFontSize * 1.5; const textAreaLineHeight = baseFontSize * 1.5;
const textAreaMinHeight = textAreaLineHeight; const textAreaMinHeight = textAreaLineHeight;
const textAreaMaxHeight = textAreaLineHeight * textAreaMaxLines; const textAreaMaxHeight = textAreaLineHeight * textAreaMaxLines;
@ -608,7 +627,7 @@ const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>(
const newHeight = Math.min(Math.max(scrollHeight, textAreaMinHeight), textAreaMaxHeight); const newHeight = Math.min(Math.max(scrollHeight, textAreaMinHeight), textAreaMaxHeight);
textAreaRef.current.style.height = newHeight + "px"; textAreaRef.current.style.height = newHeight + "px";
}, },
[termFontSize] [baseFontSize]
); );
useEffect(() => { useEffect(() => {
@ -624,7 +643,7 @@ const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>(
onMouseDown={onMouseDown} // When the user clicks on the textarea onMouseDown={onMouseDown} // When the user clicks on the textarea
onChange={onChange} onChange={onChange}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
style={{ fontSize: termFontSize }} style={{ fontSize: baseFontSize }}
placeholder="Ask anything..." placeholder="Ask anything..."
value={value} value={value}
></textarea> ></textarea>
@ -642,7 +661,7 @@ const WaveAi = ({ model }: { model: WaveAiModel; blockId: string }) => {
const [value, setValue] = useState(""); const [value, setValue] = useState("");
const [selectedBlockIdx, setSelectedBlockIdx] = useState<number | null>(null); const [selectedBlockIdx, setSelectedBlockIdx] = useState<number | null>(null);
const termFontSize: number = 14; const baseFontSize: number = 14;
const msgWidths = {}; const msgWidths = {};
const locked = useAtomValue(model.locked); const locked = useAtomValue(model.locked);
@ -804,7 +823,13 @@ const WaveAi = ({ model }: { model: WaveAiModel; blockId: string }) => {
return ( return (
<div ref={waveaiRef} className="waveai"> <div ref={waveaiRef} className="waveai">
<div className="waveai-chat"> <div className="waveai-chat">
<ChatWindow ref={osRef} chatWindowRef={chatWindowRef} messages={messages} msgWidths={msgWidths} /> <ChatWindow
ref={osRef}
chatWindowRef={chatWindowRef}
messages={messages}
msgWidths={msgWidths}
model={model}
/>
</div> </div>
<div className="waveai-controls"> <div className="waveai-controls">
<div className="waveai-input-wrapper"> <div className="waveai-input-wrapper">
@ -815,7 +840,7 @@ const WaveAi = ({ model }: { model: WaveAiModel; blockId: string }) => {
onChange={handleTextAreaChange} onChange={handleTextAreaChange}
onKeyDown={handleTextAreaKeyDown} onKeyDown={handleTextAreaKeyDown}
onMouseDown={handleTextAreaMouseDown} onMouseDown={handleTextAreaMouseDown}
termFontSize={termFontSize} baseFontSize={baseFontSize}
/> />
</div> </div>
<Button className={buttonClass} onClick={handleButtonPress}> <Button className={buttonClass} onClick={handleButtonPress}>

View File

@ -42,7 +42,7 @@
opacity: 1; opacity: 1;
} }
.iconbutton { .wave-iconbutton {
width: fit-content !important; width: fit-content !important;
margin-right: 5px; margin-right: 5px;
} }

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { BlockNodeModel } from "@/app/block/blocktypes"; import { BlockNodeModel } from "@/app/block/blocktypes";
import { getApi, getSettingsKeyAtom, openLink } from "@/app/store/global"; import { getApi, getBlockMetaKeyAtom, getSettingsKeyAtom, openLink } from "@/app/store/global";
import { getSimpleControlShiftAtom } from "@/app/store/keymodel"; import { getSimpleControlShiftAtom } from "@/app/store/keymodel";
import { ObjectService } from "@/app/store/services"; import { ObjectService } from "@/app/store/services";
import { RpcApi } from "@/app/store/wshclientapi"; import { RpcApi } from "@/app/store/wshclientapi";
@ -49,13 +49,13 @@ export class WebViewModel implements ViewModel {
mediaPlaying: PrimitiveAtom<boolean>; mediaPlaying: PrimitiveAtom<boolean>;
mediaMuted: PrimitiveAtom<boolean>; mediaMuted: PrimitiveAtom<boolean>;
modifyExternalUrl?: (url: string) => string; modifyExternalUrl?: (url: string) => string;
domReady: PrimitiveAtom<boolean>;
constructor(blockId: string, nodeModel: BlockNodeModel) { constructor(blockId: string, nodeModel: BlockNodeModel) {
this.nodeModel = nodeModel; this.nodeModel = nodeModel;
this.viewType = "web"; this.viewType = "web";
this.blockId = blockId; this.blockId = blockId;
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`); this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
this.url = atom(); this.url = atom();
const defaultUrlAtom = getSettingsKeyAtom("web:defaulturl"); const defaultUrlAtom = getSettingsKeyAtom("web:defaulturl");
this.homepageUrl = atom((get) => { this.homepageUrl = atom((get) => {
@ -71,6 +71,7 @@ export class WebViewModel implements ViewModel {
this.viewName = atom("Web"); this.viewName = atom("Web");
this.urlInputRef = createRef<HTMLInputElement>(); this.urlInputRef = createRef<HTMLInputElement>();
this.webviewRef = createRef<WebviewTag>(); this.webviewRef = createRef<WebviewTag>();
this.domReady = atom(false);
this.mediaPlaying = atom(false); this.mediaPlaying = atom(false);
this.mediaMuted = atom(false); this.mediaMuted = atom(false);
@ -339,7 +340,7 @@ export class WebViewModel implements ViewModel {
const defaultSearchAtom = getSettingsKeyAtom("web:defaultsearch"); const defaultSearchAtom = getSettingsKeyAtom("web:defaultsearch");
const searchTemplate = globalStore.get(defaultSearchAtom); const searchTemplate = globalStore.get(defaultSearchAtom);
const nextUrl = this.ensureUrlScheme(newUrl, searchTemplate); const nextUrl = this.ensureUrlScheme(newUrl, searchTemplate);
console.log("webview loadUrl", reason, nextUrl, "cur=", this.webviewRef?.current.getURL()); console.log("webview loadUrl", reason, nextUrl, "cur=", this.webviewRef.current.getURL());
if (!this.webviewRef.current) { if (!this.webviewRef.current) {
return; return;
} }
@ -414,7 +415,7 @@ export class WebViewModel implements ViewModel {
return true; return true;
} }
if (checkKeyPressed(e, "Cmd:r")) { if (checkKeyPressed(e, "Cmd:r")) {
this.webviewRef?.current?.reload(); this.webviewRef.current?.reload();
return true; return true;
} }
if (checkKeyPressed(e, "Cmd:ArrowLeft")) { if (checkKeyPressed(e, "Cmd:ArrowLeft")) {
@ -428,7 +429,61 @@ export class WebViewModel implements ViewModel {
return false; return false;
} }
setZoomFactor(factor: number | null) {
// null is ok (will reset to default)
if (factor != null && factor < 0.1) {
factor = 0.1;
}
if (factor != null && factor > 5) {
factor = 5;
}
const domReady = globalStore.get(this.domReady);
if (!domReady) {
return;
}
this.webviewRef.current?.setZoomFactor(factor || 1);
RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("block", this.blockId),
meta: { "web:zoom": factor }, // allow null so we can remove the zoom factor here
});
}
getSettingsMenuItems(): ContextMenuItem[] { getSettingsMenuItems(): ContextMenuItem[] {
const zoomSubMenu: ContextMenuItem[] = [];
let curZoom = 1;
if (globalStore.get(this.domReady)) {
curZoom = this.webviewRef.current?.getZoomFactor() || 1;
}
const model = this; // for the closure to work (this is getting unset)
function makeZoomFactorMenuItem(label: string, factor: number): ContextMenuItem {
return {
label: label,
type: "checkbox",
click: () => {
model.setZoomFactor(factor);
},
checked: curZoom == factor,
};
}
zoomSubMenu.push({
label: "Reset",
click: () => {
model.setZoomFactor(null);
},
});
zoomSubMenu.push(makeZoomFactorMenuItem("25%", 0.25));
zoomSubMenu.push(makeZoomFactorMenuItem("50%", 0.5));
zoomSubMenu.push(makeZoomFactorMenuItem("70%", 0.7));
zoomSubMenu.push(makeZoomFactorMenuItem("80%", 0.8));
zoomSubMenu.push(makeZoomFactorMenuItem("90%", 0.9));
zoomSubMenu.push(makeZoomFactorMenuItem("100%", 1));
zoomSubMenu.push(makeZoomFactorMenuItem("110%", 1.1));
zoomSubMenu.push(makeZoomFactorMenuItem("120%", 1.2));
zoomSubMenu.push(makeZoomFactorMenuItem("130%", 1.3));
zoomSubMenu.push(makeZoomFactorMenuItem("150%", 1.5));
zoomSubMenu.push(makeZoomFactorMenuItem("175%", 1.75));
zoomSubMenu.push(makeZoomFactorMenuItem("200%", 2));
return [ return [
{ {
label: "Set Block Homepage", label: "Set Block Homepage",
@ -441,6 +496,10 @@ export class WebViewModel implements ViewModel {
{ {
type: "separator", type: "separator",
}, },
{
label: "Set Zoom Factor",
submenu: zoomSubMenu,
},
{ {
label: this.webviewRef.current?.isDevToolsOpened() ? "Close DevTools" : "Open DevTools", label: this.webviewRef.current?.isDevToolsOpened() ? "Close DevTools" : "Open DevTools",
click: () => { click: () => {
@ -476,12 +535,13 @@ const WebView = memo(({ model, onFailLoad }: WebViewProps) => {
let metaUrl = blockData?.meta?.url || defaultUrl; let metaUrl = blockData?.meta?.url || defaultUrl;
metaUrl = model.ensureUrlScheme(metaUrl, defaultSearch); metaUrl = model.ensureUrlScheme(metaUrl, defaultSearch);
const metaUrlRef = useRef(metaUrl); const metaUrlRef = useRef(metaUrl);
const zoomFactor = useAtomValue(getBlockMetaKeyAtom(model.blockId, "web:zoom")) || 1;
// The initial value of the block metadata URL when the component first renders. Used to set the starting src value for the webview. // The initial value of the block metadata URL when the component first renders. Used to set the starting src value for the webview.
const [metaUrlInitial] = useState(metaUrl); const [metaUrlInitial] = useState(metaUrl);
const [webContentsId, setWebContentsId] = useState(null); const [webContentsId, setWebContentsId] = useState(null);
const [domReady, setDomReady] = useState(false); const domReady = useAtomValue(model.domReady);
const [errorText, setErrorText] = useState(""); const [errorText, setErrorText] = useState("");
@ -510,13 +570,27 @@ const WebView = memo(({ model, onFailLoad }: WebViewProps) => {
} }
useEffect(() => { useEffect(() => {
if (model.webviewRef.current && domReady) { return () => {
globalStore.set(model.domReady, false);
};
}, []);
useEffect(() => {
if (model.webviewRef.current == null || !domReady) {
return;
}
try {
const wcId = model.webviewRef.current.getWebContentsId?.(); const wcId = model.webviewRef.current.getWebContentsId?.();
if (wcId) { if (wcId) {
setWebContentsId(wcId); setWebContentsId(wcId);
if (model.webviewRef.current.getZoomFactor() != zoomFactor) {
model.webviewRef.current.setZoomFactor(zoomFactor);
} }
} }
}, [model.webviewRef.current, domReady]); } catch (e) {
console.error("Failed to get webcontentsid / setzoomlevel (webview)", e);
}
}, [model.webviewRef.current, domReady, zoomFactor]);
// Load a new URL if the block metadata is updated. // Load a new URL if the block metadata is updated.
useEffect(() => { useEffect(() => {
@ -560,7 +634,7 @@ const WebView = memo(({ model, onFailLoad }: WebViewProps) => {
console.error(errorMessage); console.error(errorMessage);
setErrorText(errorMessage); setErrorText(errorMessage);
if (onFailLoad) { if (onFailLoad) {
const curUrl = model.webviewRef?.current.getURL(); const curUrl = model.webviewRef.current.getURL();
onFailLoad(curUrl); onFailLoad(curUrl);
} }
} }
@ -573,7 +647,7 @@ const WebView = memo(({ model, onFailLoad }: WebViewProps) => {
getApi().setWebviewFocus(null); getApi().setWebviewFocus(null);
}; };
const handleDomReady = () => { const handleDomReady = () => {
setDomReady(true); globalStore.set(model.domReady, true);
setBgColor(); setBgColor();
}; };
const handleMediaPlaying = () => { const handleMediaPlaying = () => {

View File

@ -412,6 +412,11 @@ export class LayoutModel {
for (const action of actions) { for (const action of actions) {
switch (action.actiontype) { switch (action.actiontype) {
case LayoutTreeActionType.InsertNode: { case LayoutTreeActionType.InsertNode: {
if (action.ephemeral) {
this.newEphemeralNode(action.blockid);
break;
}
const insertNodeAction: LayoutTreeInsertNodeAction = { const insertNodeAction: LayoutTreeInsertNodeAction = {
type: LayoutTreeActionType.InsertNode, type: LayoutTreeActionType.InsertNode,
node: newLayoutNode(undefined, undefined, undefined, { node: newLayoutNode(undefined, undefined, undefined, {

View File

@ -401,6 +401,7 @@ declare global {
indexarr?: number[]; indexarr?: number[];
focused: boolean; focused: boolean;
magnified: boolean; magnified: boolean;
ephemeral: boolean;
}; };
// waveobj.LayoutState // waveobj.LayoutState
@ -490,6 +491,10 @@ declare global {
"term:scrollback"?: number; "term:scrollback"?: number;
"term:vdomblockid"?: string; "term:vdomblockid"?: string;
"term:vdomtoolbarblockid"?: string; "term:vdomtoolbarblockid"?: string;
"term:transparency"?: number;
"web:zoom"?: number;
"markdown:fontsize"?: number;
"markdown:fixedfontsize"?: number;
"vdom:*"?: boolean; "vdom:*"?: boolean;
"vdom:initialized"?: boolean; "vdom:initialized"?: boolean;
"vdom:correlationid"?: string; "vdom:correlationid"?: string;
@ -560,6 +565,14 @@ declare global {
prompt: OpenAIPromptMessageType[]; prompt: OpenAIPromptMessageType[];
}; };
// wshrpc.PathCommandData
type PathCommandData = {
pathtype: string;
open: boolean;
openexternal: boolean;
tabid: string;
};
// waveobj.Point // waveobj.Point
type Point = { type Point = {
x: number; x: number;
@ -604,6 +617,9 @@ declare global {
// wconfig.SettingsType // wconfig.SettingsType
type SettingsType = { type SettingsType = {
"app:*"?: boolean;
"app:globalhotkey"?: string;
"app:dismissarchitecturewarning"?: boolean;
"ai:*"?: boolean; "ai:*"?: boolean;
"ai:preset"?: string; "ai:preset"?: string;
"ai:apitype"?: string; "ai:apitype"?: string;
@ -615,6 +631,8 @@ declare global {
"ai:apiversion"?: string; "ai:apiversion"?: string;
"ai:maxtokens"?: number; "ai:maxtokens"?: number;
"ai:timeoutms"?: number; "ai:timeoutms"?: number;
"ai:fontsize"?: number;
"ai:fixedfontsize"?: number;
"term:*"?: boolean; "term:*"?: boolean;
"term:fontsize"?: number; "term:fontsize"?: number;
"term:fontfamily"?: string; "term:fontfamily"?: string;
@ -624,9 +642,11 @@ declare global {
"term:localshellopts"?: string[]; "term:localshellopts"?: string[];
"term:scrollback"?: number; "term:scrollback"?: number;
"term:copyonselect"?: boolean; "term:copyonselect"?: boolean;
"term:transparency"?: number;
"editor:minimapenabled"?: boolean; "editor:minimapenabled"?: boolean;
"editor:stickyscrollenabled"?: boolean; "editor:stickyscrollenabled"?: boolean;
"editor:wordwrap"?: boolean; "editor:wordwrap"?: boolean;
"editor:fontsize"?: number;
"web:*"?: boolean; "web:*"?: boolean;
"web:openlinksinternally"?: boolean; "web:openlinksinternally"?: boolean;
"web:defaulturl"?: string; "web:defaulturl"?: string;
@ -638,6 +658,8 @@ declare global {
"autoupdate:intervalms"?: number; "autoupdate:intervalms"?: number;
"autoupdate:installonquit"?: boolean; "autoupdate:installonquit"?: boolean;
"autoupdate:channel"?: string; "autoupdate:channel"?: string;
"markdown:fontsize"?: number;
"markdown:fixedfontsize"?: number;
"preview:showhiddenfiles"?: boolean; "preview:showhiddenfiles"?: boolean;
"tab:preset"?: string; "tab:preset"?: string;
"widget:*"?: boolean; "widget:*"?: boolean;

5
go.mod
View File

@ -18,8 +18,9 @@ require (
github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/mapstructure v1.5.0
github.com/sashabaranov/go-openai v1.36.0 github.com/sashabaranov/go-openai v1.36.0
github.com/sawka/txwrap v0.2.0 github.com/sawka/txwrap v0.2.0
github.com/shirou/gopsutil/v4 v4.24.10 github.com/shirou/gopsutil/v4 v4.24.11
github.com/skeema/knownhosts v1.3.0 github.com/skeema/knownhosts v1.3.0
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/spf13/cobra v1.8.1 github.com/spf13/cobra v1.8.1
github.com/ubuntu/gowsl v0.0.0-20240906163211-049fd49bd93b github.com/ubuntu/gowsl v0.0.0-20240906163211-049fd49bd93b
github.com/wavetermdev/htmltoken v0.2.0 github.com/wavetermdev/htmltoken v0.2.0
@ -47,6 +48,6 @@ require (
golang.org/x/net v0.29.0 // indirect golang.org/x/net v0.29.0 // indirect
) )
replace github.com/kevinburke/ssh_config => github.com/wavetermdev/ssh_config v0.0.0-20241027232332-ed124367682d replace github.com/kevinburke/ssh_config => github.com/wavetermdev/ssh_config v0.0.0-20241219203747-6409e4292f34
replace github.com/creack/pty => github.com/photostorm/pty v1.1.19-0.20230903182454-31354506054b replace github.com/creack/pty => github.com/photostorm/pty v1.1.19-0.20230903182454-31354506054b

10
go.sum
View File

@ -62,12 +62,14 @@ github.com/sashabaranov/go-openai v1.36.0 h1:fcSrn8uGuorzPWCBp8L0aCR95Zjb/Dd+ZSM
github.com/sashabaranov/go-openai v1.36.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/sashabaranov/go-openai v1.36.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/sawka/txwrap v0.2.0 h1:V3LfvKVLULxcYSxdMguLwFyQFMEU9nFDJopg0ZkL+94= github.com/sawka/txwrap v0.2.0 h1:V3LfvKVLULxcYSxdMguLwFyQFMEU9nFDJopg0ZkL+94=
github.com/sawka/txwrap v0.2.0/go.mod h1:wwQ2SQiN4U+6DU/iVPhbvr7OzXAtgZlQCIGuvOswEfA= github.com/sawka/txwrap v0.2.0/go.mod h1:wwQ2SQiN4U+6DU/iVPhbvr7OzXAtgZlQCIGuvOswEfA=
github.com/shirou/gopsutil/v4 v4.24.10 h1:7VOzPtfw/5YDU+jLEoBwXwxJbQetULywoSV4RYY7HkM= github.com/shirou/gopsutil/v4 v4.24.11 h1:WaU9xqGFKvFfsUv94SXcUPD7rCkU0vr/asVdQOBZNj8=
github.com/shirou/gopsutil/v4 v4.24.10/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8= github.com/shirou/gopsutil/v4 v4.24.11/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY=
github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@ -88,8 +90,8 @@ github.com/ubuntu/gowsl v0.0.0-20240906163211-049fd49bd93b h1:wFBKF5k5xbJQU8bYgc
github.com/ubuntu/gowsl v0.0.0-20240906163211-049fd49bd93b/go.mod h1:N1CYNinssZru+ikvYTgVbVeSi21thHUTCoJ9xMvWe+s= github.com/ubuntu/gowsl v0.0.0-20240906163211-049fd49bd93b/go.mod h1:N1CYNinssZru+ikvYTgVbVeSi21thHUTCoJ9xMvWe+s=
github.com/wavetermdev/htmltoken v0.2.0 h1:sFVPPemlDv7/jg7n4Hx1AEF2m9MVAFjFpELWfhi/DlM= github.com/wavetermdev/htmltoken v0.2.0 h1:sFVPPemlDv7/jg7n4Hx1AEF2m9MVAFjFpELWfhi/DlM=
github.com/wavetermdev/htmltoken v0.2.0/go.mod h1:5FM0XV6zNYiNza2iaTcFGj+hnMtgqumFHO31Z8euquk= github.com/wavetermdev/htmltoken v0.2.0/go.mod h1:5FM0XV6zNYiNza2iaTcFGj+hnMtgqumFHO31Z8euquk=
github.com/wavetermdev/ssh_config v0.0.0-20241027232332-ed124367682d h1:ArHaUBaiQWUqBzM2G/oLlm3Be0kwUMDt9vTNOWIfOd0= github.com/wavetermdev/ssh_config v0.0.0-20241219203747-6409e4292f34 h1:I8VZVTZEXhnzfN7jB9a7TZYpzNO48sCUWMRXHM9XWSA=
github.com/wavetermdev/ssh_config v0.0.0-20241027232332-ed124367682d/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/wavetermdev/ssh_config v0.0.0-20241219203747-6409e4292f34/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=

View File

@ -7,7 +7,7 @@
"productName": "Wave", "productName": "Wave",
"description": "Open-Source AI-Native Terminal Built for Seamless Workflows", "description": "Open-Source AI-Native Terminal Built for Seamless Workflows",
"license": "Apache-2.0", "license": "Apache-2.0",
"version": "0.10.1", "version": "0.10.2-beta.2",
"homepage": "https://waveterm.dev", "homepage": "https://waveterm.dev",
"build": { "build": {
"appId": "dev.commandline.waveterm" "appId": "dev.commandline.waveterm"
@ -43,7 +43,7 @@
"@types/css-tree": "^2", "@types/css-tree": "^2",
"@types/debug": "^4", "@types/debug": "^4",
"@types/electron": "^1.6.12", "@types/electron": "^1.6.12",
"@types/node": "^22.10.1", "@types/node": "^22.10.2",
"@types/papaparse": "^5", "@types/papaparse": "^5",
"@types/pngjs": "^6.0.5", "@types/pngjs": "^6.0.5",
"@types/prop-types": "^15", "@types/prop-types": "^15",
@ -58,7 +58,7 @@
"@types/ws": "^8", "@types/ws": "^8",
"@vitejs/plugin-react-swc": "^3.7.2", "@vitejs/plugin-react-swc": "^3.7.2",
"@vitest/coverage-istanbul": "^2.1.8", "@vitest/coverage-istanbul": "^2.1.8",
"electron": "^33.2.0", "electron": "^33.2.1",
"electron-builder": "^25.1.8", "electron-builder": "^25.1.8",
"electron-vite": "^2.3.0", "electron-vite": "^2.3.0",
"eslint": "^9.16.0", "eslint": "^9.16.0",
@ -67,7 +67,7 @@
"prettier-plugin-jsdoc": "^1.3.0", "prettier-plugin-jsdoc": "^1.3.0",
"prettier-plugin-organize-imports": "^4.1.0", "prettier-plugin-organize-imports": "^4.1.0",
"rollup-plugin-flow": "^1.1.1", "rollup-plugin-flow": "^1.1.1",
"sass": "^1.82.0", "sass": "^1.83.0",
"semver": "^7.6.3", "semver": "^7.6.3",
"storybook": "^8.4.7", "storybook": "^8.4.7",
"storybook-dark-mode": "^4.0.2", "storybook-dark-mode": "^4.0.2",
@ -75,12 +75,12 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"tsx": "^4.19.2", "tsx": "^4.19.2",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"typescript-eslint": "^8.17.0", "typescript-eslint": "^8.18.0",
"vite": "^6.0.2", "vite": "^6.0.3",
"vite-plugin-image-optimizer": "^1.1.8", "vite-plugin-image-optimizer": "^1.1.8",
"vite-plugin-static-copy": "^2.2.0", "vite-plugin-static-copy": "^2.2.0",
"vite-plugin-svgr": "^4.3.0", "vite-plugin-svgr": "^4.3.0",
"vite-tsconfig-paths": "^5.1.3", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^2.1.8" "vitest": "^2.1.8"
}, },
"dependencies": { "dependencies": {
@ -116,6 +116,7 @@
"overlayscrollbars": "^2.10.1", "overlayscrollbars": "^2.10.1",
"overlayscrollbars-react": "^0.5.6", "overlayscrollbars-react": "^0.5.6",
"papaparse": "^5.4.1", "papaparse": "^5.4.1",
"parse-srcset": "^1.0.2",
"pngjs": "^7.0.0", "pngjs": "^7.0.0",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "^18.3.1", "react": "^18.3.1",

View File

@ -227,6 +227,9 @@ func (entry *CacheEntry) readAt(ctx context.Context, offset int64, size int64, r
offset += truncateAmt offset += truncateAmt
size -= truncateAmt size -= truncateAmt
} }
if size <= 0 {
return realDataOffset, nil, nil
}
} }
partMap := file.computePartMap(offset, size) partMap := file.computePartMap(offset, size)
dataEntryMap, err := entry.loadDataPartsForRead(ctx, getPartIdxsFromMap(partMap)) dataEntryMap, err := entry.loadDataPartsForRead(ctx, getPartIdxsFromMap(partMap))

View File

@ -509,11 +509,6 @@ func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.Conn
conn.WithLock(func() { conn.WithLock(func() {
conn.Client = client conn.Client = client
}) })
err = conn.OpenDomainSocketListener()
if err != nil {
log.Printf("error: unable to open domain socket listener for %s: %v\n", conn.GetName(), err)
return err
}
config := wconfig.ReadFullConfig() config := wconfig.ReadFullConfig()
enableWsh := config.Settings.ConnWshEnabled enableWsh := config.Settings.ConnWshEnabled
askBeforeInstall := config.Settings.ConnAskBeforeWshInstall askBeforeInstall := config.Settings.ConnAskBeforeWshInstall
@ -545,15 +540,22 @@ func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.Conn
} }
if conn.WshEnabled.Load() { if conn.WshEnabled.Load() {
csErr := conn.StartConnServer() dsErr := conn.OpenDomainSocketListener()
var csErr error
if dsErr != nil {
log.Printf("error: unable to open domain socket listener for %s: %v\n", conn.GetName(), dsErr)
} else {
csErr = conn.StartConnServer()
if csErr != nil { if csErr != nil {
log.Printf("error: unable to start conn server for %s: %v\n", conn.GetName(), csErr) log.Printf("error: unable to start conn server for %s: %v\n", conn.GetName(), csErr)
}
}
if dsErr != nil || csErr != nil {
log.Print("attempting to run with nowsh instead") log.Print("attempting to run with nowsh instead")
conn.WithLock(func() { conn.WithLock(func() {
conn.WshError = csErr.Error() conn.WshError = csErr.Error()
}) })
conn.WshEnabled.Store(false) conn.WshEnabled.Store(false)
//return fmt.Errorf("conncontroller %s start wsh connserver error: %v", conn.GetName(), csErr)
} }
} }
} else { } else {

View File

@ -23,6 +23,7 @@ import (
"github.com/kevinburke/ssh_config" "github.com/kevinburke/ssh_config"
"github.com/skeema/knownhosts" "github.com/skeema/knownhosts"
"github.com/wavetermdev/waveterm/pkg/panichandler"
"github.com/wavetermdev/waveterm/pkg/trimquotes" "github.com/wavetermdev/waveterm/pkg/trimquotes"
"github.com/wavetermdev/waveterm/pkg/userinput" "github.com/wavetermdev/waveterm/pkg/userinput"
"github.com/wavetermdev/waveterm/pkg/util/shellutil" "github.com/wavetermdev/waveterm/pkg/util/shellutil"
@ -773,7 +774,13 @@ func combineSshKeywords(userProvidedOpts *wshrpc.ConnKeywords, configKeywords *w
// note that a `var == "yes"` will default to false // note that a `var == "yes"` will default to false
// but `var != "no"` will default to true // but `var != "no"` will default to true
// when given unexpected strings // when given unexpected strings
func findSshConfigKeywords(hostPattern string) (*wshrpc.ConnKeywords, error) { func findSshConfigKeywords(hostPattern string) (connKeywords *wshrpc.ConnKeywords, outErr error) {
defer func() {
err := panichandler.PanicHandler("sshclient:find-ssh-config-keywords")
if err != nil {
outErr = err
}
}()
WaveSshConfigUserSettings().ReloadConfigs() WaveSshConfigUserSettings().ReloadConfigs()
sshKeywords := &wshrpc.ConnKeywords{} sshKeywords := &wshrpc.ConnKeywords{}
var err error var err error

View File

@ -50,6 +50,9 @@ func (svc *WorkspaceService) UpdateWorkspace(ctx context.Context, workspaceId st
return nil, fmt.Errorf("error updating workspace: %w", err) return nil, fmt.Errorf("error updating workspace: %w", err)
} }
wps.Broker.Publish(wps.WaveEvent{
Event: wps.Event_WorkspaceUpdate})
updates := waveobj.ContextGetUpdatesRtn(ctx) updates := waveobj.ContextGetUpdatesRtn(ctx)
go func() { go func() {
defer panichandler.PanicHandler("WorkspaceService:UpdateWorkspace:SendUpdateEvents") defer panichandler.PanicHandler("WorkspaceService:UpdateWorkspace:SendUpdateEvents")

View File

@ -167,9 +167,6 @@ func StartWslShellProc(ctx context.Context, termSize waveobj.TermSize, cmdStr st
homeDir := wsl.GetHomeDir(conn.Context, client) homeDir := wsl.GetHomeDir(conn.Context, client)
shellOpts = append(shellOpts, "~", "-d", client.Name()) shellOpts = append(shellOpts, "~", "-d", client.Name())
if isZshShell(shellPath) {
shellOpts = append(shellOpts, fmt.Sprintf(`ZDOTDIR="%s/.waveterm/%s"`, homeDir, shellutil.ZshIntegrationDir))
}
var subShellOpts []string var subShellOpts []string
if cmdStr == "" { if cmdStr == "" {
@ -216,6 +213,10 @@ func StartWslShellProc(ctx context.Context, termSize waveobj.TermSize, cmdStr st
} else { } else {
shellOpts = append(shellOpts, "--", fmt.Sprintf(`%s=%s`, wshutil.WaveJwtTokenVarName, jwtToken)) shellOpts = append(shellOpts, "--", fmt.Sprintf(`%s=%s`, wshutil.WaveJwtTokenVarName, jwtToken))
} }
if isZshShell(shellPath) {
shellOpts = append(shellOpts, fmt.Sprintf(`ZDOTDIR=%s/.waveterm/%s`, homeDir, shellutil.ZshIntegrationDir))
}
shellOpts = append(shellOpts, shellPath) shellOpts = append(shellOpts, shellPath)
shellOpts = append(shellOpts, subShellOpts...) shellOpts = append(shellOpts, subShellOpts...)
log.Printf("full cmd is: %s %s", "wsl.exe", strings.Join(shellOpts, " ")) log.Printf("full cmd is: %s %s", "wsl.exe", strings.Join(shellOpts, " "))

View File

@ -93,6 +93,12 @@ const (
MetaKey_TermScrollback = "term:scrollback" MetaKey_TermScrollback = "term:scrollback"
MetaKey_TermVDomSubBlockId = "term:vdomblockid" MetaKey_TermVDomSubBlockId = "term:vdomblockid"
MetaKey_TermVDomToolbarBlockId = "term:vdomtoolbarblockid" MetaKey_TermVDomToolbarBlockId = "term:vdomtoolbarblockid"
MetaKey_TermTransparency = "term:transparency"
MetaKey_WebZoom = "web:zoom"
MetaKey_MarkdownFontSize = "markdown:fontsize"
MetaKey_MarkdownFixedFontSize = "markdown:fixedfontsize"
MetaKey_VDomClear = "vdom:*" MetaKey_VDomClear = "vdom:*"
MetaKey_VDomInitialized = "vdom:initialized" MetaKey_VDomInitialized = "vdom:initialized"

View File

@ -208,6 +208,7 @@ type LayoutActionData struct {
IndexArr *[]int `json:"indexarr,omitempty"` IndexArr *[]int `json:"indexarr,omitempty"`
Focused bool `json:"focused"` Focused bool `json:"focused"`
Magnified bool `json:"magnified"` Magnified bool `json:"magnified"`
Ephemeral bool `json:"ephemeral"`
} }
type LeafOrderEntry struct { type LeafOrderEntry struct {

View File

@ -94,6 +94,12 @@ type MetaTSType struct {
TermScrollback *int `json:"term:scrollback,omitempty"` TermScrollback *int `json:"term:scrollback,omitempty"`
TermVDomSubBlockId string `json:"term:vdomblockid,omitempty"` TermVDomSubBlockId string `json:"term:vdomblockid,omitempty"`
TermVDomToolbarBlockId string `json:"term:vdomtoolbarblockid,omitempty"` TermVDomToolbarBlockId string `json:"term:vdomtoolbarblockid,omitempty"`
TermTransparency *float64 `json:"term:transparency,omitempty"` // default 0.5
WebZoom float64 `json:"web:zoom,omitempty"`
MarkdownFontSize float64 `json:"markdown:fontsize,omitempty"`
MarkdownFixedFontSize float64 `json:"markdown:fixedfontsize,omitempty"`
VDomClear bool `json:"vdom:*,omitempty"` VDomClear bool `json:"vdom:*,omitempty"`
VDomInitialized bool `json:"vdom:initialized,omitempty"` VDomInitialized bool `json:"vdom:initialized,omitempty"`

View File

@ -22,12 +22,38 @@
"cmdtext": "#f0f0f0", "cmdtext": "#f0f0f0",
"foreground": "#c1c1c1", "foreground": "#c1c1c1",
"selectionBackground": "", "selectionBackground": "",
"background": "#00000077", "background": "#000000",
"cursor": "" "cursor": ""
}, },
"onedarkpro": {
"display:name": "One Dark Pro",
"display:order": 2,
"background": "#282C34",
"foreground": "#ABB2BF",
"cursor": "#D7DAE0",
"selectionBackground": "#528BFF",
"black": "#3F4451",
"red": "#E05561",
"green": "#8CC265",
"yellow": "#D18F52",
"blue": "#4AA5F0",
"magenta": "#C162DE",
"cyan": "#42B3C2",
"white": "#D7DAE0",
"brightBlack": "#4F5666",
"brightRed": "#FF616E",
"brightGreen": "#A5E075",
"brightYellow": "#F0A45D",
"brightBlue": "#4DC4FF",
"brightMagenta": "#DE73FF",
"brightCyan": "#4CD1E0",
"brightWhite": "#E6E6E6",
"gray": "#495162",
"cmdtext": "#ABB2BF"
},
"dracula": { "dracula": {
"display:name": "Dracula", "display:name": "Dracula",
"display:order": 2, "display:order": 3,
"black": "#21222C", "black": "#21222C",
"red": "#FF5555", "red": "#FF5555",
"green": "#50FA7B", "green": "#50FA7B",
@ -53,7 +79,7 @@
}, },
"monokai": { "monokai": {
"display:name": "Monokai", "display:name": "Monokai",
"display:order": 3, "display:order": 4,
"black": "#1B1D1E", "black": "#1B1D1E",
"red": "#F92672", "red": "#F92672",
"green": "#A6E22E", "green": "#A6E22E",
@ -79,7 +105,7 @@
}, },
"campbell": { "campbell": {
"display:name": "Campbell", "display:name": "Campbell",
"display:order": 4, "display:order": 5,
"black": "#0C0C0C", "black": "#0C0C0C",
"red": "#C50F1F", "red": "#C50F1F",
"green": "#13A10E", "green": "#13A10E",
@ -105,7 +131,7 @@
}, },
"warmyellow": { "warmyellow": {
"display:name": "Warm Yellow", "display:name": "Warm Yellow",
"display:order": 4, "display:order": 6,
"black": "#3C3228", "black": "#3C3228",
"red": "#E67E22", "red": "#E67E22",
"green": "#A5D6A7", "green": "#A5D6A7",
@ -127,30 +153,30 @@
"selectionBackground": "#B7950B", "selectionBackground": "#B7950B",
"cursor": "#F9D784" "cursor": "#F9D784"
}, },
"onedarkpro": { "rosepine": {
"display:name": "One Dark Pro", "display:name": "Rose Pine",
"display:order": 1.5, "display:order": 7,
"background": "#282C34", "black": "#26233a",
"foreground": "#ABB2BF", "red": "#eb6f92",
"cursor": "#D7DAE0", "green": "#3e8fb0",
"selectionBackground": "#528BFF", "yellow": "#f6c177",
"black": "#3F4451", "blue": "#9ccfd8",
"red": "#E05561", "magenta": "#c4a7e7",
"green": "#8CC265", "cyan": "#ebbcba",
"yellow": "#D18F52", "white": "#e0def4",
"blue": "#4AA5F0", "brightBlack": "#908caa",
"magenta": "#C162DE", "brightRed": "#ff8cab",
"cyan": "#42B3C2", "brightGreen": "#9ccfb0",
"white": "#D7DAE0", "brightYellow": "#ffd196",
"brightBlack": "#4F5666", "brightBlue": "#bee6e0",
"brightRed": "#FF616E", "brightMagenta": "#e2c4ff",
"brightGreen": "#A5E075", "brightCyan": "#ffd1d0",
"brightYellow": "#F0A45D", "brightWhite": "#fffaf3",
"brightBlue": "#4DC4FF", "gray": "#908caa",
"brightMagenta": "#DE73FF", "cmdtext": "#e0def4",
"brightCyan": "#4CD1E0", "foreground": "#e0def4",
"brightWhite": "#E6E6E6", "selectionBackground": "#403d52",
"gray": "#495162", "background": "#191724",
"cmdtext": "#ABB2BF" "cursor": "#524f67"
} }
} }

View File

@ -6,6 +6,10 @@
package wconfig package wconfig
const ( const (
ConfigKey_AppClear = "app:*"
ConfigKey_AppGlobalHotkey = "app:globalhotkey"
ConfigKey_AppDismissArchitectureWarning = "app:dismissarchitecturewarning"
ConfigKey_AiClear = "ai:*" ConfigKey_AiClear = "ai:*"
ConfigKey_AiPreset = "ai:preset" ConfigKey_AiPreset = "ai:preset"
ConfigKey_AiApiType = "ai:apitype" ConfigKey_AiApiType = "ai:apitype"
@ -17,6 +21,8 @@ const (
ConfigKey_AIApiVersion = "ai:apiversion" ConfigKey_AIApiVersion = "ai:apiversion"
ConfigKey_AiMaxTokens = "ai:maxtokens" ConfigKey_AiMaxTokens = "ai:maxtokens"
ConfigKey_AiTimeoutMs = "ai:timeoutms" ConfigKey_AiTimeoutMs = "ai:timeoutms"
ConfigKey_AiFontSize = "ai:fontsize"
ConfigKey_AiFixedFontSize = "ai:fixedfontsize"
ConfigKey_TermClear = "term:*" ConfigKey_TermClear = "term:*"
ConfigKey_TermFontSize = "term:fontsize" ConfigKey_TermFontSize = "term:fontsize"
@ -27,10 +33,12 @@ const (
ConfigKey_TermLocalShellOpts = "term:localshellopts" ConfigKey_TermLocalShellOpts = "term:localshellopts"
ConfigKey_TermScrollback = "term:scrollback" ConfigKey_TermScrollback = "term:scrollback"
ConfigKey_TermCopyOnSelect = "term:copyonselect" ConfigKey_TermCopyOnSelect = "term:copyonselect"
ConfigKey_TermTransparency = "term:transparency"
ConfigKey_EditorMinimapEnabled = "editor:minimapenabled" ConfigKey_EditorMinimapEnabled = "editor:minimapenabled"
ConfigKey_EditorStickyScrollEnabled = "editor:stickyscrollenabled" ConfigKey_EditorStickyScrollEnabled = "editor:stickyscrollenabled"
ConfigKey_EditorWordWrap = "editor:wordwrap" ConfigKey_EditorWordWrap = "editor:wordwrap"
ConfigKey_EditorFontSize = "editor:fontsize"
ConfigKey_WebClear = "web:*" ConfigKey_WebClear = "web:*"
ConfigKey_WebOpenLinksInternally = "web:openlinksinternally" ConfigKey_WebOpenLinksInternally = "web:openlinksinternally"
@ -46,6 +54,9 @@ const (
ConfigKey_AutoUpdateInstallOnQuit = "autoupdate:installonquit" ConfigKey_AutoUpdateInstallOnQuit = "autoupdate:installonquit"
ConfigKey_AutoUpdateChannel = "autoupdate:channel" ConfigKey_AutoUpdateChannel = "autoupdate:channel"
ConfigKey_MarkdownFontSize = "markdown:fontsize"
ConfigKey_MarkdownFixedFontSize = "markdown:fixedfontsize"
ConfigKey_PreviewShowHiddenFiles = "preview:showhiddenfiles" ConfigKey_PreviewShowHiddenFiles = "preview:showhiddenfiles"
ConfigKey_TabPreset = "tab:preset" ConfigKey_TabPreset = "tab:preset"

View File

@ -33,6 +33,10 @@ const AnySchema = `
` `
type SettingsType struct { type SettingsType struct {
AppClear bool `json:"app:*,omitempty"`
AppGlobalHotkey string `json:"app:globalhotkey,omitempty"`
AppDismissArchitectureWarning bool `json:"app:dismissarchitecturewarning,omitempty"`
AiClear bool `json:"ai:*,omitempty"` AiClear bool `json:"ai:*,omitempty"`
AiPreset string `json:"ai:preset,omitempty"` AiPreset string `json:"ai:preset,omitempty"`
AiApiType string `json:"ai:apitype,omitempty"` AiApiType string `json:"ai:apitype,omitempty"`
@ -44,6 +48,8 @@ type SettingsType struct {
AIApiVersion string `json:"ai:apiversion,omitempty"` AIApiVersion string `json:"ai:apiversion,omitempty"`
AiMaxTokens float64 `json:"ai:maxtokens,omitempty"` AiMaxTokens float64 `json:"ai:maxtokens,omitempty"`
AiTimeoutMs float64 `json:"ai:timeoutms,omitempty"` AiTimeoutMs float64 `json:"ai:timeoutms,omitempty"`
AiFontSize float64 `json:"ai:fontsize,omitempty"`
AiFixedFontSize float64 `json:"ai:fixedfontsize,omitempty"`
TermClear bool `json:"term:*,omitempty"` TermClear bool `json:"term:*,omitempty"`
TermFontSize float64 `json:"term:fontsize,omitempty"` TermFontSize float64 `json:"term:fontsize,omitempty"`
@ -54,10 +60,12 @@ type SettingsType struct {
TermLocalShellOpts []string `json:"term:localshellopts,omitempty"` TermLocalShellOpts []string `json:"term:localshellopts,omitempty"`
TermScrollback *int64 `json:"term:scrollback,omitempty"` TermScrollback *int64 `json:"term:scrollback,omitempty"`
TermCopyOnSelect *bool `json:"term:copyonselect,omitempty"` TermCopyOnSelect *bool `json:"term:copyonselect,omitempty"`
TermTransparency *float64 `json:"term:transparency,omitempty"`
EditorMinimapEnabled bool `json:"editor:minimapenabled,omitempty"` EditorMinimapEnabled bool `json:"editor:minimapenabled,omitempty"`
EditorStickyScrollEnabled bool `json:"editor:stickyscrollenabled,omitempty"` EditorStickyScrollEnabled bool `json:"editor:stickyscrollenabled,omitempty"`
EditorWordWrap bool `json:"editor:wordwrap,omitempty"` EditorWordWrap bool `json:"editor:wordwrap,omitempty"`
EditorFontSize float64 `json:"editor:fontsize,omitempty"`
WebClear bool `json:"web:*,omitempty"` WebClear bool `json:"web:*,omitempty"`
WebOpenLinksInternally bool `json:"web:openlinksinternally,omitempty"` WebOpenLinksInternally bool `json:"web:openlinksinternally,omitempty"`
@ -73,6 +81,9 @@ type SettingsType struct {
AutoUpdateInstallOnQuit bool `json:"autoupdate:installonquit,omitempty"` AutoUpdateInstallOnQuit bool `json:"autoupdate:installonquit,omitempty"`
AutoUpdateChannel string `json:"autoupdate:channel,omitempty"` AutoUpdateChannel string `json:"autoupdate:channel,omitempty"`
MarkdownFontSize float64 `json:"markdown:fontsize,omitempty"`
MarkdownFixedFontSize float64 `json:"markdown:fixedfontsize,omitempty"`
PreviewShowHiddenFiles *bool `json:"preview:showhiddenfiles,omitempty"` PreviewShowHiddenFiles *bool `json:"preview:showhiddenfiles,omitempty"`
TabPreset string `json:"tab:preset,omitempty"` TabPreset string `json:"tab:preset,omitempty"`

View File

@ -28,7 +28,8 @@ func SwitchWorkspace(ctx context.Context, windowId string, workspaceId string) (
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting window: %w", err) return nil, fmt.Errorf("error getting window: %w", err)
} }
if window.WorkspaceId == workspaceId { curWsId := window.WorkspaceId
if curWsId == workspaceId {
return nil, nil return nil, nil
} }
@ -45,24 +46,24 @@ func SwitchWorkspace(ctx context.Context, windowId string, workspaceId string) (
return nil, err return nil, err
} }
} }
window.WorkspaceId = workspaceId
curWs, err := GetWorkspace(ctx, window.WorkspaceId) err = wstore.DBUpdate(ctx, window)
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting current workspace: %w", err) return nil, fmt.Errorf("error updating window: %w", err)
} }
deleted, err := DeleteWorkspace(ctx, curWs.OID, false)
deleted, err := DeleteWorkspace(ctx, curWsId, false)
if err != nil { if err != nil {
return nil, fmt.Errorf("error deleting current workspace: %w", err) return nil, fmt.Errorf("error deleting current workspace: %w", err)
} }
if !deleted { if !deleted {
log.Printf("current workspace %s was not deleted\n", curWs.OID) log.Printf("current workspace %s was not deleted\n", curWsId)
} else { } else {
log.Printf("deleted current workspace %s\n", curWs.OID) log.Printf("deleted current workspace %s\n", curWsId)
} }
window.WorkspaceId = workspaceId
log.Printf("switching window %s to workspace %s\n", windowId, workspaceId) log.Printf("switching window %s to workspace %s\n", windowId, workspaceId)
return ws, wstore.DBUpdate(ctx, window) return ws, nil
} }
func GetWindow(ctx context.Context, windowId string) (*waveobj.Window, error) { func GetWindow(ctx context.Context, windowId string) (*waveobj.Window, error) {

View File

@ -122,6 +122,7 @@ func DeleteWorkspace(ctx context.Context, workspaceId string, force bool) (bool,
return false, fmt.Errorf("error closing tab: %w", err) return false, fmt.Errorf("error closing tab: %w", err)
} }
} }
windowId, err := wstore.DBFindWindowForWorkspaceId(ctx, workspaceId)
err = wstore.DBDelete(ctx, waveobj.OType_Workspace, workspaceId) err = wstore.DBDelete(ctx, waveobj.OType_Workspace, workspaceId)
if err != nil { if err != nil {
return false, fmt.Errorf("error deleting workspace: %w", err) return false, fmt.Errorf("error deleting workspace: %w", err)
@ -129,6 +130,12 @@ func DeleteWorkspace(ctx context.Context, workspaceId string, force bool) (bool,
log.Printf("deleted workspace %s\n", workspaceId) log.Printf("deleted workspace %s\n", workspaceId)
wps.Broker.Publish(wps.WaveEvent{ wps.Broker.Publish(wps.WaveEvent{
Event: wps.Event_WorkspaceUpdate}) Event: wps.Event_WorkspaceUpdate})
if windowId != "" {
err = CloseWindow(ctx, windowId, false)
if err != nil {
return false, fmt.Errorf("error closing window: %w", err)
}
}
return true, nil return true, nil
} }

View File

@ -247,6 +247,12 @@ func NotifyCommand(w *wshutil.WshRpc, data wshrpc.WaveNotificationOptions, opts
return err return err
} }
// command "path", wshserver.PathCommand
func PathCommand(w *wshutil.WshRpc, data wshrpc.PathCommandData, opts *wshrpc.RpcOpts) (string, error) {
resp, err := sendRpcRequestCallHelper[string](w, "path", data, opts)
return resp, err
}
// command "remotefiledelete", wshserver.RemoteFileDeleteCommand // command "remotefiledelete", wshserver.RemoteFileDeleteCommand
func RemoteFileDeleteCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error { func RemoteFileDeleteCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "remotefiledelete", data, opts) _, err := sendRpcRequestCallHelper[any](w, "remotefiledelete", data, opts)

View File

@ -149,6 +149,7 @@ type WshRpcInterface interface {
ActivityCommand(ctx context.Context, data ActivityUpdate) error ActivityCommand(ctx context.Context, data ActivityUpdate) error
GetVarCommand(ctx context.Context, data CommandVarData) (*CommandVarResponseData, error) GetVarCommand(ctx context.Context, data CommandVarData) (*CommandVarResponseData, error)
SetVarCommand(ctx context.Context, data CommandVarData) error SetVarCommand(ctx context.Context, data CommandVarData) error
PathCommand(ctx context.Context, data PathCommandData) (string, error)
// connection functions // connection functions
ConnStatusCommand(ctx context.Context) ([]ConnStatus, error) ConnStatusCommand(ctx context.Context) ([]ConnStatus, error)
@ -605,6 +606,13 @@ type CommandVarResponseData struct {
Exists bool `json:"exists"` Exists bool `json:"exists"`
} }
type PathCommandData struct {
PathType string `json:"pathtype"`
Open bool `json:"open"`
OpenExternal bool `json:"openexternal"`
TabId string `json:"tabid" wshcontext:"TabId"`
}
type ActivityDisplayType struct { type ActivityDisplayType struct {
Width int `json:"width"` Width int `json:"width"`
Height int `json:"height"` Height int `json:"height"`

View File

@ -12,10 +12,12 @@ import (
"fmt" "fmt"
"io/fs" "io/fs"
"log" "log"
"path/filepath"
"regexp" "regexp"
"strings" "strings"
"time" "time"
"github.com/skratchdot/open-golang/open"
"github.com/wavetermdev/waveterm/pkg/blockcontroller" "github.com/wavetermdev/waveterm/pkg/blockcontroller"
"github.com/wavetermdev/waveterm/pkg/filestore" "github.com/wavetermdev/waveterm/pkg/filestore"
"github.com/wavetermdev/waveterm/pkg/panichandler" "github.com/wavetermdev/waveterm/pkg/panichandler"
@ -183,6 +185,7 @@ func (ws *WshServer) CreateBlockCommand(ctx context.Context, data wshrpc.Command
ActionType: wcore.LayoutActionDataType_Insert, ActionType: wcore.LayoutActionDataType_Insert,
BlockId: blockData.OID, BlockId: blockData.OID,
Magnified: data.Magnified, Magnified: data.Magnified,
Ephemeral: data.Ephemeral,
Focused: true, Focused: true,
}) })
if err != nil { if err != nil {
@ -829,3 +832,39 @@ func (ws *WshServer) SetVarCommand(ctx context.Context, data wshrpc.CommandVarDa
envStr := envutil.MapToEnv(envMap) envStr := envutil.MapToEnv(envMap)
return filestore.WFS.WriteFile(ctx, data.ZoneId, data.FileName, []byte(envStr)) return filestore.WFS.WriteFile(ctx, data.ZoneId, data.FileName, []byte(envStr))
} }
func (ws *WshServer) PathCommand(ctx context.Context, data wshrpc.PathCommandData) (string, error) {
pathType := data.PathType
openInternal := data.Open
openExternal := data.OpenExternal
var path string
switch pathType {
case "config":
path = wavebase.GetWaveConfigDir()
case "data":
path = wavebase.GetWaveDataDir()
case "log":
path = filepath.Join(wavebase.GetWaveDataDir(), "waveapp.log")
}
if openInternal && openExternal {
return "", fmt.Errorf("open and openExternal cannot both be true")
}
if openInternal {
_, err := ws.CreateBlockCommand(ctx, wshrpc.CommandCreateBlockData{BlockDef: &waveobj.BlockDef{Meta: map[string]any{
waveobj.MetaKey_View: "preview",
waveobj.MetaKey_File: path,
}}, Ephemeral: true, TabId: data.TabId})
if err != nil {
return path, fmt.Errorf("error opening path: %w", err)
}
} else if openExternal {
err := open.Run(path)
if err != nil {
return path, fmt.Errorf("error opening path: %w", err)
}
}
return path, nil
}

View File

@ -342,7 +342,10 @@ func (w *WshRpc) runServer() {
continue continue
} }
if msg.IsRpcRequest() { if msg.IsRpcRequest() {
go w.handleRequest(&msg) go func() {
defer panichandler.PanicHandler("handleRequest:goroutine")
w.handleRequest(&msg)
}()
} else { } else {
respCh := w.getResponseCh(msg.ResId) respCh := w.getResponseCh(msg.ResId)
if respCh == nil { if respCh == nil {

207
yarn.lock
View File

@ -6450,12 +6450,12 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/node@npm:^22.10.1": "@types/node@npm:^22.10.2":
version: 22.10.1 version: 22.10.2
resolution: "@types/node@npm:22.10.1" resolution: "@types/node@npm:22.10.2"
dependencies: dependencies:
undici-types: "npm:~6.20.0" undici-types: "npm:~6.20.0"
checksum: 10c0/0fbb6d29fa35d807f0223a4db709c598ac08d66820240a2cd6a8a69b8f0bc921d65b339d850a666b43b4e779f967e6ed6cf6f0fca3575e08241e6b900364c234 checksum: 10c0/2c7b71a040f1ef5320938eca8ebc946e6905caa9bbf3d5665d9b3774a8d15ea9fab1582b849a6d28c7fc80756a62c5666bc66b69f42f4d5dafd1ccb193cdb4ac
languageName: node languageName: node
linkType: hard linkType: hard
@ -6502,9 +6502,9 @@ __metadata:
linkType: hard linkType: hard
"@types/prop-types@npm:*, @types/prop-types@npm:^15": "@types/prop-types@npm:*, @types/prop-types@npm:^15":
version: 15.7.13 version: 15.7.14
resolution: "@types/prop-types@npm:15.7.13" resolution: "@types/prop-types@npm:15.7.14"
checksum: 10c0/1b20fc67281902c6743379960247bc161f3f0406ffc0df8e7058745a85ea1538612109db0406290512947f9632fe9e10e7337bf0ce6338a91d6c948df16a7c61 checksum: 10c0/1ec775160bfab90b67a782d735952158c7e702ca4502968aa82565bd8e452c2de8601c8dfe349733073c31179116cf7340710160d3836aa8a1ef76d1532893b1
languageName: node languageName: node
linkType: hard linkType: hard
@ -6793,15 +6793,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/eslint-plugin@npm:8.17.0": "@typescript-eslint/eslint-plugin@npm:8.18.0":
version: 8.17.0 version: 8.18.0
resolution: "@typescript-eslint/eslint-plugin@npm:8.17.0" resolution: "@typescript-eslint/eslint-plugin@npm:8.18.0"
dependencies: dependencies:
"@eslint-community/regexpp": "npm:^4.10.0" "@eslint-community/regexpp": "npm:^4.10.0"
"@typescript-eslint/scope-manager": "npm:8.17.0" "@typescript-eslint/scope-manager": "npm:8.18.0"
"@typescript-eslint/type-utils": "npm:8.17.0" "@typescript-eslint/type-utils": "npm:8.18.0"
"@typescript-eslint/utils": "npm:8.17.0" "@typescript-eslint/utils": "npm:8.18.0"
"@typescript-eslint/visitor-keys": "npm:8.17.0" "@typescript-eslint/visitor-keys": "npm:8.18.0"
graphemer: "npm:^1.4.0" graphemer: "npm:^1.4.0"
ignore: "npm:^5.3.1" ignore: "npm:^5.3.1"
natural-compare: "npm:^1.4.0" natural-compare: "npm:^1.4.0"
@ -6809,108 +6809,99 @@ __metadata:
peerDependencies: peerDependencies:
"@typescript-eslint/parser": ^8.0.0 || ^8.0.0-alpha.0 "@typescript-eslint/parser": ^8.0.0 || ^8.0.0-alpha.0
eslint: ^8.57.0 || ^9.0.0 eslint: ^8.57.0 || ^9.0.0
peerDependenciesMeta: typescript: ">=4.8.4 <5.8.0"
typescript: checksum: 10c0/c338da1b96c41d7b94401a6711659d0fef3acb691eff7a958f9d3aa0442a858830daad67e3575288a4f4669572e2b690517a513519b404a465ad68fe0a82d3ec
optional: true
checksum: 10c0/d78778173571a9a1370345bc2aa3e850235a489d16b8a8b5ba3086b988bbef7549bdae38e509d7a679ba3179c688cc5a408376b158be402770836e94ffc9602d
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/parser@npm:8.17.0": "@typescript-eslint/parser@npm:8.18.0":
version: 8.17.0 version: 8.18.0
resolution: "@typescript-eslint/parser@npm:8.17.0" resolution: "@typescript-eslint/parser@npm:8.18.0"
dependencies: dependencies:
"@typescript-eslint/scope-manager": "npm:8.17.0" "@typescript-eslint/scope-manager": "npm:8.18.0"
"@typescript-eslint/types": "npm:8.17.0" "@typescript-eslint/types": "npm:8.18.0"
"@typescript-eslint/typescript-estree": "npm:8.17.0" "@typescript-eslint/typescript-estree": "npm:8.18.0"
"@typescript-eslint/visitor-keys": "npm:8.17.0" "@typescript-eslint/visitor-keys": "npm:8.18.0"
debug: "npm:^4.3.4" debug: "npm:^4.3.4"
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 eslint: ^8.57.0 || ^9.0.0
peerDependenciesMeta: typescript: ">=4.8.4 <5.8.0"
typescript: checksum: 10c0/d3a062511c24dfcf522a645db1153022d49aa3bb05e288c22474cf04dc1d836f877eb9d2733947e448981ffb16e4de50d4ebe7570a268733a641f228ca6c4849
optional: true
checksum: 10c0/2543deadf01302a92d3b6f58a4c14f98d8936c4d976e7da05e3bb65608f19d8de93b25282e343c304eca3e3f37f2ac23e97fa9c11c6edff36dd2d4f6b601a630
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/scope-manager@npm:8.17.0": "@typescript-eslint/scope-manager@npm:8.18.0":
version: 8.17.0 version: 8.18.0
resolution: "@typescript-eslint/scope-manager@npm:8.17.0" resolution: "@typescript-eslint/scope-manager@npm:8.18.0"
dependencies: dependencies:
"@typescript-eslint/types": "npm:8.17.0" "@typescript-eslint/types": "npm:8.18.0"
"@typescript-eslint/visitor-keys": "npm:8.17.0" "@typescript-eslint/visitor-keys": "npm:8.18.0"
checksum: 10c0/0c08d14240bad4b3f6874f08ba80b29db1a6657437089a6f109db458c544d835bcdc06ba9140bb4f835233ba4326d9a86e6cf6bdb5209960d2f7025aa3191f4f checksum: 10c0/6bf6532fd43f2b55b9b47fa8b0217c5b5a03f022e869a6a21228fc3ae04c0ac6c5ae5d6026866d189ba424d2f98cc6fbd2a34f909d241c9b86c031afd808f90c
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/type-utils@npm:8.17.0": "@typescript-eslint/type-utils@npm:8.18.0":
version: 8.17.0 version: 8.18.0
resolution: "@typescript-eslint/type-utils@npm:8.17.0" resolution: "@typescript-eslint/type-utils@npm:8.18.0"
dependencies: dependencies:
"@typescript-eslint/typescript-estree": "npm:8.17.0" "@typescript-eslint/typescript-estree": "npm:8.18.0"
"@typescript-eslint/utils": "npm:8.17.0" "@typescript-eslint/utils": "npm:8.18.0"
debug: "npm:^4.3.4" debug: "npm:^4.3.4"
ts-api-utils: "npm:^1.3.0" ts-api-utils: "npm:^1.3.0"
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 eslint: ^8.57.0 || ^9.0.0
peerDependenciesMeta: typescript: ">=4.8.4 <5.8.0"
typescript: checksum: 10c0/c0fcf201c3b53f9374c0571198a639c81536170141caa08fd0f47094a596b1f82f839a849eac5832f954345c567dccb45b2ee1c0872c513331165f7bcb812396
optional: true
checksum: 10c0/6138ec71b5692d4b5e0bf3d7f66a6fa4e91ddea7031907b0ac45a7693df0a2f4cc5bca7218311e0639620d636ceb7efec83a137dfcd5938304d873b774fcc8bd
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/types@npm:8.17.0": "@typescript-eslint/types@npm:8.18.0":
version: 8.17.0 version: 8.18.0
resolution: "@typescript-eslint/types@npm:8.17.0" resolution: "@typescript-eslint/types@npm:8.18.0"
checksum: 10c0/26b1bf9dfc3ee783c85c6f354b84c28706d5689d777f3ff2de2cb496e45f9d0189c0d561c03ccbc8b24712438be17cf63dd0871ff3ca2083e7f48749770d1893 checksum: 10c0/2dd7468c3f1c305545268b72c3a333488e6ab1b628c5f65081d895866422b9376c21634a7aac437805f84b22e352b6a8fc4dcf925ef4a8fd7d1898b8359f71be
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/typescript-estree@npm:8.17.0": "@typescript-eslint/typescript-estree@npm:8.18.0":
version: 8.17.0 version: 8.18.0
resolution: "@typescript-eslint/typescript-estree@npm:8.17.0" resolution: "@typescript-eslint/typescript-estree@npm:8.18.0"
dependencies: dependencies:
"@typescript-eslint/types": "npm:8.17.0" "@typescript-eslint/types": "npm:8.18.0"
"@typescript-eslint/visitor-keys": "npm:8.17.0" "@typescript-eslint/visitor-keys": "npm:8.18.0"
debug: "npm:^4.3.4" debug: "npm:^4.3.4"
fast-glob: "npm:^3.3.2" fast-glob: "npm:^3.3.2"
is-glob: "npm:^4.0.3" is-glob: "npm:^4.0.3"
minimatch: "npm:^9.0.4" minimatch: "npm:^9.0.4"
semver: "npm:^7.6.0" semver: "npm:^7.6.0"
ts-api-utils: "npm:^1.3.0" ts-api-utils: "npm:^1.3.0"
peerDependenciesMeta: peerDependencies:
typescript: typescript: ">=4.8.4 <5.8.0"
optional: true checksum: 10c0/87b432b190b627314f007b17b2371898db78baaa3df67a0d9a94d080d88a7a307906b54a735084cacef37f6421e2b9c3320040617e73fe54eac2bf22c610f1ec
checksum: 10c0/523013f9b5cf2c58c566868e4c3b0b9ac1b4807223a6d64e2a7c58e01e53b6587ba61f1a8241eade361f3f426d6057657515473176141ef8aebb352bc0d223ce
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/utils@npm:8.17.0": "@typescript-eslint/utils@npm:8.18.0":
version: 8.17.0 version: 8.18.0
resolution: "@typescript-eslint/utils@npm:8.17.0" resolution: "@typescript-eslint/utils@npm:8.18.0"
dependencies: dependencies:
"@eslint-community/eslint-utils": "npm:^4.4.0" "@eslint-community/eslint-utils": "npm:^4.4.0"
"@typescript-eslint/scope-manager": "npm:8.17.0" "@typescript-eslint/scope-manager": "npm:8.18.0"
"@typescript-eslint/types": "npm:8.17.0" "@typescript-eslint/types": "npm:8.18.0"
"@typescript-eslint/typescript-estree": "npm:8.17.0" "@typescript-eslint/typescript-estree": "npm:8.18.0"
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 eslint: ^8.57.0 || ^9.0.0
peerDependenciesMeta: typescript: ">=4.8.4 <5.8.0"
typescript: checksum: 10c0/58a2fc1e404d1f905c2a958d995824eb4abc6e73836b186717550677f8b1d17954acc369feddb83277350915388bc3d8b721423c37777b8b8017fc29c89ec6ee
optional: true
checksum: 10c0/a9785ae5f7e7b51d521dc3f48b15093948e4fcd03352c0b60f39bae366cbc935947d215f91e2ae3182d52fa6affb5ccbb50feff487bd1209011f3e0da02cdf07
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/visitor-keys@npm:8.17.0": "@typescript-eslint/visitor-keys@npm:8.18.0":
version: 8.17.0 version: 8.18.0
resolution: "@typescript-eslint/visitor-keys@npm:8.17.0" resolution: "@typescript-eslint/visitor-keys@npm:8.18.0"
dependencies: dependencies:
"@typescript-eslint/types": "npm:8.17.0" "@typescript-eslint/types": "npm:8.18.0"
eslint-visitor-keys: "npm:^4.2.0" eslint-visitor-keys: "npm:^4.2.0"
checksum: 10c0/9144c4e4a63034fb2031a0ee1fc77e80594f30cab3faafa9a1f7f83782695774dd32fac8986f260698b4e150b4dd52444f2611c07e4c101501f08353eb47c82c checksum: 10c0/d4cdc2adab553098b5be7117fb7df76fb66cfd380528881a0a8c2a9eee03bf8baddda07d15ca0bd3ed8b35c379b3f449292183df18e3e81898dbcadafcb708b8
languageName: node languageName: node
linkType: hard linkType: hard
@ -10507,16 +10498,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"electron@npm:^33.2.0": "electron@npm:^33.2.1":
version: 33.2.0 version: 33.3.0
resolution: "electron@npm:33.2.0" resolution: "electron@npm:33.3.0"
dependencies: dependencies:
"@electron/get": "npm:^2.0.0" "@electron/get": "npm:^2.0.0"
"@types/node": "npm:^20.9.0" "@types/node": "npm:^20.9.0"
extract-zip: "npm:^2.0.1" extract-zip: "npm:^2.0.1"
bin: bin:
electron: cli.js electron: cli.js
checksum: 10c0/ad013bcc4fe65f6e0f40b77387452966347d4fb54acc8d8d7b0310244f73511a7ac87e03e1097970f83b94bcafc44e0b98a4c603daa9c95e11931d7c020ff155 checksum: 10c0/ce381fb63da60b52b131aea473568aad2bff58cc3d8fd6fc68b4d0c0d5485e381e1a576727477ce901f4bd017baaa9e66f3f04da6ff698229343193258a8d38e
languageName: node languageName: node
linkType: hard linkType: hard
@ -16299,6 +16290,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"parse-srcset@npm:^1.0.2":
version: 1.0.2
resolution: "parse-srcset@npm:1.0.2"
checksum: 10c0/2f268e3d110d4c53d06ed2a8e8ee61a7da0cee13bf150819a6da066a8ca9b8d15b5600d6e6cae8be940e2edc50ee7c1e1052934d6ec858324065ecef848f0497
languageName: node
linkType: hard
"parse5-htmlparser2-tree-adapter@npm:^7.0.0": "parse5-htmlparser2-tree-adapter@npm:^7.0.0":
version: 7.1.0 version: 7.1.0
resolution: "parse5-htmlparser2-tree-adapter@npm:7.1.0" resolution: "parse5-htmlparser2-tree-adapter@npm:7.1.0"
@ -19358,9 +19356,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"sass@npm:^1.82.0": "sass@npm:^1.83.0":
version: 1.82.0 version: 1.83.0
resolution: "sass@npm:1.82.0" resolution: "sass@npm:1.83.0"
dependencies: dependencies:
"@parcel/watcher": "npm:^2.4.1" "@parcel/watcher": "npm:^2.4.1"
chokidar: "npm:^4.0.0" chokidar: "npm:^4.0.0"
@ -19371,7 +19369,7 @@ __metadata:
optional: true optional: true
bin: bin:
sass: sass.js sass: sass.js
checksum: 10c0/7f86fe6ade4f6018862c448ed69d5c52f485b0125c9dab24e63f679739a04cc7c56562d588e3cf16b5efb4d2c4d0530e62740e1cfd273e2e3707d04d11011736 checksum: 10c0/4415361229879a9041d77c953da85482e89032aa4321ba13250a9987d39c80fac6c88af3777f2a2d76a4e8b0c8afbd21c1970fdbe84e0b3ec25fb26741f92beb
languageName: node languageName: node
linkType: hard linkType: hard
@ -20962,19 +20960,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"typescript-eslint@npm:^8.17.0": "typescript-eslint@npm:^8.18.0":
version: 8.17.0 version: 8.18.0
resolution: "typescript-eslint@npm:8.17.0" resolution: "typescript-eslint@npm:8.18.0"
dependencies: dependencies:
"@typescript-eslint/eslint-plugin": "npm:8.17.0" "@typescript-eslint/eslint-plugin": "npm:8.18.0"
"@typescript-eslint/parser": "npm:8.17.0" "@typescript-eslint/parser": "npm:8.18.0"
"@typescript-eslint/utils": "npm:8.17.0" "@typescript-eslint/utils": "npm:8.18.0"
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 eslint: ^8.57.0 || ^9.0.0
peerDependenciesMeta: typescript: ">=4.8.4 <5.8.0"
typescript: checksum: 10c0/dda882cbfc1ebad6903864571bc69bfd7e32e17fec67d98fdfab2bd652348d425c6a1c3697734d59cd5dd15d26d496db3c3808c1de5840fa29b9e76184fa1865
optional: true
checksum: 10c0/b148525769b9afa789ad3c2d52249fa78e67a225d48d17f2f0117b0e8b52566112be3a35de6cd26bcaffba3114be87c1070f7f4b4e2b730c059668fec4a530bc
languageName: node languageName: node
linkType: hard linkType: hard
@ -21675,9 +21671,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"vite-tsconfig-paths@npm:^5.1.3": "vite-tsconfig-paths@npm:^5.1.4":
version: 5.1.3 version: 5.1.4
resolution: "vite-tsconfig-paths@npm:5.1.3" resolution: "vite-tsconfig-paths@npm:5.1.4"
dependencies: dependencies:
debug: "npm:^4.1.1" debug: "npm:^4.1.1"
globrex: "npm:^0.1.2" globrex: "npm:^0.1.2"
@ -21687,7 +21683,7 @@ __metadata:
peerDependenciesMeta: peerDependenciesMeta:
vite: vite:
optional: true optional: true
checksum: 10c0/fb7480efa31fd50439f4a12c91bc953e5cc09d69fdc7eeb6ffff7cc796bc2c1f2c617c3abfdcbf5d7414848076cea9deb60bc002142f93b6e3131e5458760710 checksum: 10c0/6228f23155ea25d92b1e1702284cf8dc52ad3c683c5ca691edd5a4c82d2913e7326d00708cef1cbfde9bb226261df0e0a12e03ef1d43b6a92d8f02b483ef37e3
languageName: node languageName: node
linkType: hard linkType: hard
@ -21734,9 +21730,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"vite@npm:^6.0.2": "vite@npm:^6.0.3":
version: 6.0.2 version: 6.0.3
resolution: "vite@npm:6.0.2" resolution: "vite@npm:6.0.3"
dependencies: dependencies:
esbuild: "npm:^0.24.0" esbuild: "npm:^0.24.0"
fsevents: "npm:~2.3.3" fsevents: "npm:~2.3.3"
@ -21782,7 +21778,7 @@ __metadata:
optional: true optional: true
bin: bin:
vite: bin/vite.js vite: bin/vite.js
checksum: 10c0/45fc609f2bc5fb5beb5a8e2cad9ad6c2edce229a922f6fc1270ea2a9d75819482edcc0f77c85b4a7abdad7eb69ce6a4f26131925d47cdc0778fc15d1bbc3b6a2 checksum: 10c0/764ebed14770426a638575b23a51127c630ace873999ab896b0184484d8107e7255cdf64cfb36c65c1ef1d583e44b70a1d14c0f05b89612e834a5806e3964475
languageName: node languageName: node
linkType: hard linkType: hard
@ -21937,7 +21933,7 @@ __metadata:
remark-preset-lint-recommended: "npm:^7.0.0" remark-preset-lint-recommended: "npm:^7.0.0"
remark-typescript-code-import: "npm:^1.0.1" remark-typescript-code-import: "npm:^1.0.1"
typescript: "npm:^5.7.2" typescript: "npm:^5.7.2"
typescript-eslint: "npm:^8.17.0" typescript-eslint: "npm:^8.18.0"
ua-parser-js: "npm:^2.0.0" ua-parser-js: "npm:^2.0.0"
languageName: unknown languageName: unknown
linkType: soft linkType: soft
@ -21970,7 +21966,7 @@ __metadata:
"@types/css-tree": "npm:^2" "@types/css-tree": "npm:^2"
"@types/debug": "npm:^4" "@types/debug": "npm:^4"
"@types/electron": "npm:^1.6.12" "@types/electron": "npm:^1.6.12"
"@types/node": "npm:^22.10.1" "@types/node": "npm:^22.10.2"
"@types/papaparse": "npm:^5" "@types/papaparse": "npm:^5"
"@types/pngjs": "npm:^6.0.5" "@types/pngjs": "npm:^6.0.5"
"@types/prop-types": "npm:^15" "@types/prop-types": "npm:^15"
@ -21997,7 +21993,7 @@ __metadata:
css-tree: "npm:^3.0.1" css-tree: "npm:^3.0.1"
dayjs: "npm:^1.11.13" dayjs: "npm:^1.11.13"
debug: "npm:^4.3.7" debug: "npm:^4.3.7"
electron: "npm:^33.2.0" electron: "npm:^33.2.1"
electron-builder: "npm:^25.1.8" electron-builder: "npm:^25.1.8"
electron-updater: "npm:6.3.9" electron-updater: "npm:6.3.9"
electron-vite: "npm:^2.3.0" electron-vite: "npm:^2.3.0"
@ -22014,6 +22010,7 @@ __metadata:
overlayscrollbars: "npm:^2.10.1" overlayscrollbars: "npm:^2.10.1"
overlayscrollbars-react: "npm:^0.5.6" overlayscrollbars-react: "npm:^0.5.6"
papaparse: "npm:^5.4.1" papaparse: "npm:^5.4.1"
parse-srcset: "npm:^1.0.2"
pngjs: "npm:^7.0.0" pngjs: "npm:^7.0.0"
prettier: "npm:^3.4.2" prettier: "npm:^3.4.2"
prettier-plugin-jsdoc: "npm:^1.3.0" prettier-plugin-jsdoc: "npm:^1.3.0"
@ -22035,7 +22032,7 @@ __metadata:
remark-github-blockquote-alert: "npm:^1.3.0" remark-github-blockquote-alert: "npm:^1.3.0"
rollup-plugin-flow: "npm:^1.1.1" rollup-plugin-flow: "npm:^1.1.1"
rxjs: "npm:^7.8.1" rxjs: "npm:^7.8.1"
sass: "npm:^1.82.0" sass: "npm:^1.83.0"
semver: "npm:^7.6.3" semver: "npm:^7.6.3"
sharp: "npm:^0.33.5" sharp: "npm:^0.33.5"
shell-quote: "npm:^1.8.2" shell-quote: "npm:^1.8.2"
@ -22048,13 +22045,13 @@ __metadata:
tslib: "npm:^2.8.1" tslib: "npm:^2.8.1"
tsx: "npm:^4.19.2" tsx: "npm:^4.19.2"
typescript: "npm:^5.7.2" typescript: "npm:^5.7.2"
typescript-eslint: "npm:^8.17.0" typescript-eslint: "npm:^8.18.0"
use-device-pixel-ratio: "npm:^1.1.2" use-device-pixel-ratio: "npm:^1.1.2"
vite: "npm:^6.0.2" vite: "npm:^6.0.3"
vite-plugin-image-optimizer: "npm:^1.1.8" vite-plugin-image-optimizer: "npm:^1.1.8"
vite-plugin-static-copy: "npm:^2.2.0" vite-plugin-static-copy: "npm:^2.2.0"
vite-plugin-svgr: "npm:^4.3.0" vite-plugin-svgr: "npm:^4.3.0"
vite-tsconfig-paths: "npm:^5.1.3" vite-tsconfig-paths: "npm:^5.1.4"
vitest: "npm:^2.1.8" vitest: "npm:^2.1.8"
winston: "npm:^3.17.0" winston: "npm:^3.17.0"
ws: "npm:^8.18.0" ws: "npm:^8.18.0"