mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
merge main 'main' into branch
This commit is contained in:
commit
44ff4e9071
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -20,6 +20,9 @@
|
||||
"[less]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[scss]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
|
24
README.md
24
README.md
@ -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)
|
||||
[![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)
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
|
||||
Wave Terminal and WSH run on the following platforms:
|
||||
Wave Terminal runs on the following platforms:
|
||||
|
||||
- macOS 11 or later (arm64, 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)
|
||||
|
||||
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
|
||||
|
||||
- Homepage — https://www.waveterm.dev
|
||||
@ -43,6 +60,7 @@ Wave Terminal and WSH run on the following platforms:
|
||||
- Documentation — https://docs.waveterm.dev
|
||||
- Legacy Documentation — https://legacydocs.waveterm.dev
|
||||
- Blog — https://blog.waveterm.dev
|
||||
- X — https://x.com/wavetermdev
|
||||
- Discord Community — https://discord.gg/XfvZ334gwU
|
||||
|
||||
## Building from Source
|
||||
|
@ -96,7 +96,7 @@ tasks:
|
||||
cmds:
|
||||
- cmd: '{{.RMRF}} "make"'
|
||||
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:
|
||||
- yarn
|
||||
- docsite:build:embedded
|
||||
|
135
cmd/wsh/cmd/wshcmd-wavepath.go
Normal file
135
cmd/wsh/cmd/wshcmd-wavepath.go
Normal 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
|
||||
}
|
@ -25,6 +25,8 @@ wsh editconfig
|
||||
|
||||
| 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:baseurl | string | Set the AI Base Url (must be OpenAI compatible) |
|
||||
| ai:apitoken | string | your AI api token |
|
||||
@ -40,12 +42,16 @@ wsh editconfig
|
||||
| term:fontfamily | string | font family to use for terminal block |
|
||||
| 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: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: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: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) |
|
||||
| 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: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 |
|
||||
@ -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 |
|
||||
| cursorAccent | CSS color | | | color for cursor |
|
||||
| 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 <code> </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`
|
||||
|
@ -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. |
|
||||
| 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
|
||||
|
||||
The `wsh` command gives some commands specifically for interacting with the connections. You can view these [here](/wsh-reference#conn).
|
||||
|
@ -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: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 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
|
||||
|
||||
@ -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:
|
||||
|
||||
```json
|
||||
{
|
||||
<... other widgets go here ...>,
|
||||
"dua" : {
|
||||
"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 ...>
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```json
|
||||
{
|
||||
<... other widgets go here ...>,
|
||||
"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 ...>
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```json
|
||||
{
|
||||
<... other widgets go here ...>,
|
||||
"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 ...>
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```json
|
||||
{
|
||||
<... other widgets go here ...>,
|
||||
"3min-info" : {
|
||||
"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 ...>
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```json
|
||||
{
|
||||
<... other widgets go here ...>,
|
||||
"all-cpu" : {
|
||||
"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 ...>
|
||||
}
|
||||
```
|
||||
|
||||
This adds an icon to the widget bar that you can press to launch All CPU plots by default.
|
||||
|
@ -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:
|
||||
|
||||
```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
|
||||
```
|
||||
|
@ -14,6 +14,35 @@ Wave Terminal is a modern terminal that includes graphical capabilities like web
|
||||
<PlatformProvider>
|
||||
<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
|
||||
|
||||
<PlatformItem platforms={["mac"]}>
|
||||
|
BIN
docs/docs/img/widget-example-fish.webp
Normal file
BIN
docs/docs/img/widget-example-fish.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 127 KiB |
BIN
docs/docs/img/widget-example-pwsh.webp
Normal file
BIN
docs/docs/img/widget-example-pwsh.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 129 KiB |
@ -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 |
|
||||
|
||||
## 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>
|
||||
|
@ -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.
|
||||
|
||||
## 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>
|
||||
|
@ -53,7 +53,7 @@
|
||||
"remark-preset-lint-consistent": "^6.0.0",
|
||||
"remark-preset-lint-recommended": "^7.0.0",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.17.0"
|
||||
"typescript-eslint": "^8.18.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"path-to-regexp@npm:2.2.1": "^3",
|
||||
|
@ -166,3 +166,90 @@ export function ensureBoundsAreVisible(bounds: electron.Rectangle): electron.Rec
|
||||
}
|
||||
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("+");
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
import { ClientService, FileService, ObjectService, WindowService, WorkspaceService } from "@/app/store/services";
|
||||
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 { debounce } from "throttle-debounce";
|
||||
import {
|
||||
@ -14,7 +14,7 @@ import {
|
||||
setWasInFg,
|
||||
} from "./emain-activity";
|
||||
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 { getElectronAppBasePath, unamePlatform } from "./platform";
|
||||
import { updater } from "./updater";
|
||||
@ -766,3 +766,23 @@ export async function relaunchBrowserWindows() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ import {
|
||||
getWaveWindowById,
|
||||
getWaveWindowByWebContentsId,
|
||||
getWaveWindowByWorkspaceId,
|
||||
registerGlobalHotkey,
|
||||
relaunchBrowserWindows,
|
||||
WaveBrowserWindow,
|
||||
} from "./emain-window";
|
||||
@ -46,6 +47,7 @@ import { getLaunchSettings } from "./launchsettings";
|
||||
import { log } from "./log";
|
||||
import { makeAppMenu } from "./menu";
|
||||
import {
|
||||
checkIfRunningUnderARM64Translation,
|
||||
getElectronAppBasePath,
|
||||
getElectronAppUnpackedBasePath,
|
||||
getWaveConfigDir,
|
||||
@ -587,6 +589,7 @@ async function appMain() {
|
||||
await electronApp.whenReady();
|
||||
configureAuthKeyRequestInjection(electron.session.defaultSession);
|
||||
const fullConfig = await services.FileService.GetFullConfig();
|
||||
checkIfRunningUnderARM64Translation(fullConfig);
|
||||
ensureHotSpareTab(fullConfig);
|
||||
await relaunchBrowserWindows();
|
||||
await initDocsite();
|
||||
@ -610,6 +613,10 @@ async function appMain() {
|
||||
fireAndForget(createNewWaveWindow);
|
||||
}
|
||||
});
|
||||
const rawGlobalHotKey = launchSettings?.["app:globalhotkey"];
|
||||
if (rawGlobalHotKey) {
|
||||
registerGlobalHotkey(rawGlobalHotKey);
|
||||
}
|
||||
}
|
||||
|
||||
appMain().catch((e) => {
|
||||
|
@ -1,7 +1,8 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// 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 { existsSync, mkdirSync } from "fs";
|
||||
import os from "os";
|
||||
@ -40,6 +41,32 @@ const WaveConfigHomeVarName = "WAVETERM_CONFIG_HOME";
|
||||
const WaveDataHomeVarName = "WAVETERM_DATA_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`).
|
||||
* @returns The path to the directory if it exists and contains valid data for the current app, otherwise null.
|
||||
|
@ -124,9 +124,9 @@
|
||||
opacity: 0.7;
|
||||
flex-grow: 1;
|
||||
|
||||
&.flex-nogrow {
|
||||
flex-grow: 0;
|
||||
}
|
||||
&.flex-nogrow {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
&.preview-filename {
|
||||
direction: rtl;
|
||||
@ -166,7 +166,7 @@
|
||||
flex: 1 2 auto;
|
||||
overflow: hidden;
|
||||
padding-right: 4px;
|
||||
@include mixins.ellipsis()
|
||||
@include mixins.ellipsis();
|
||||
}
|
||||
|
||||
.connecting-svg {
|
||||
@ -211,12 +211,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
.wave-button {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
// webview specific. for refresh button
|
||||
.iconbutton {
|
||||
.wave-iconbutton {
|
||||
height: 100%;
|
||||
width: 27px;
|
||||
display: flex;
|
||||
@ -226,7 +226,7 @@
|
||||
}
|
||||
|
||||
.menubutton {
|
||||
.button {
|
||||
.wave-button {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
@ -236,7 +236,7 @@
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
|
||||
.iconbutton {
|
||||
.wave-iconbutton {
|
||||
display: flex;
|
||||
width: 24px;
|
||||
padding: 4px 6px;
|
||||
@ -268,7 +268,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.iconbutton {
|
||||
.wave-iconbutton {
|
||||
opacity: 0.7;
|
||||
font-size: 45px;
|
||||
margin: -30px 0 0 0;
|
||||
@ -373,7 +373,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.button:last-child {
|
||||
.wave-button:last-child {
|
||||
margin-top: 1.5px;
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
@use "../mixins.scss";
|
||||
|
||||
.button {
|
||||
.wave-button {
|
||||
// override default button appearance
|
||||
border: 1px solid transparent;
|
||||
outline: 1px solid transparent;
|
||||
|
@ -32,7 +32,7 @@ const Button = memo(
|
||||
<Component
|
||||
ref={btnRef}
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
className={clsx("button", finalClassName)}
|
||||
className={clsx("wave-button", finalClassName)}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
>
|
||||
|
@ -1,4 +1,4 @@
|
||||
.iconbutton {
|
||||
.wave-iconbutton {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
|
@ -16,7 +16,7 @@ export const IconButton = memo(
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={clsx("iconbutton", className, decl.className, {
|
||||
className={clsx("wave-iconbutton", className, decl.className, {
|
||||
disabled: decl.disabled,
|
||||
"no-action": decl.noAction,
|
||||
})}
|
||||
|
@ -1,6 +1,12 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// 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 = {
|
||||
type: string;
|
||||
id: string;
|
||||
@ -147,3 +153,56 @@ export function transformBlocks(content: string): { content: string; blocks: Map
|
||||
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(", ");
|
||||
};
|
||||
|
@ -16,21 +16,70 @@
|
||||
overflow: scroll;
|
||||
line-height: 1.5;
|
||||
color: var(--main-text-color);
|
||||
font-family: var(--markdown-font);
|
||||
font-size: 14px;
|
||||
font-family: var(--markdown-font-family);
|
||||
font-size: var(--markdown-font-size);
|
||||
overflow-wrap: break-word;
|
||||
|
||||
&.non-scrollable {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
*:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.heading:not(.heading ~ .heading) {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.heading {
|
||||
&:first-of-type {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
color: var(--main-text-color);
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
margin-top: 1.143em;
|
||||
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 {
|
||||
@ -44,24 +93,24 @@
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
list-style-position: outside;
|
||||
margin-left: 16px;
|
||||
margin-left: 1.143em;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-position: outside;
|
||||
margin-left: 19px;
|
||||
margin-left: 1.357em;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 4px 10px 4px 10px;
|
||||
border-radius: 3px;
|
||||
margin: 0.286em 0.714em;
|
||||
border-radius: 4px;
|
||||
background-color: var(--panel-bg-color);
|
||||
padding: 2px 4px 2px 6px;
|
||||
padding: 0.143em 0.286em 0.143em 0.429em;
|
||||
}
|
||||
|
||||
pre.codeblock {
|
||||
background-color: var(--panel-bg-color);
|
||||
margin: 4px 10px;
|
||||
margin: 0.286em 0.714em;
|
||||
padding: 0.4em 0.7em;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
@ -83,11 +132,11 @@
|
||||
right: 0;
|
||||
border-radius: 4px;
|
||||
backdrop-filter: blur(8px);
|
||||
margin: 2px 2px;
|
||||
padding: 4px 4px;
|
||||
margin: 0.143em;
|
||||
padding: 0.286em;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
gap: 0.286em;
|
||||
}
|
||||
|
||||
&:hover .codeblock-actions {
|
||||
@ -98,6 +147,7 @@
|
||||
code {
|
||||
color: var(--main-text-color);
|
||||
font: var(--fixed-font);
|
||||
font-size: var(--markdown-fixed-font-size);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@ -105,41 +155,13 @@
|
||||
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 {
|
||||
margin: 16px 0;
|
||||
margin: 1.143em 0;
|
||||
|
||||
.wave-block-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
padding: 0.857em;
|
||||
background-color: var(--highlight-bg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
@ -150,15 +172,15 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 2.857em;
|
||||
height: 2.857em;
|
||||
background-color: black;
|
||||
border-radius: 8px;
|
||||
margin-right: 12px;
|
||||
margin-right: 0.857em;
|
||||
}
|
||||
|
||||
.wave-block-icon i {
|
||||
font-size: 18px;
|
||||
font-size: 1.125em;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
@ -168,19 +190,18 @@
|
||||
}
|
||||
|
||||
.wave-block-filename {
|
||||
font-size: 14px;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.wave-block-size {
|
||||
font-size: 12px;
|
||||
font-size: 0.857em;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The TOC view should scroll independently of the contents view.
|
||||
.toc {
|
||||
max-width: 40%;
|
||||
height: 100%;
|
||||
@ -192,21 +213,20 @@
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
gap: 0.357em;
|
||||
text-wrap: wrap;
|
||||
|
||||
h4 {
|
||||
padding-left: 5px;
|
||||
padding-left: 0.357em;
|
||||
}
|
||||
|
||||
.toc-item {
|
||||
cursor: pointer;
|
||||
--indent-factor: 1;
|
||||
|
||||
// The 5px offset in the padding will ensure that when the text in the item wraps, it indents slightly.
|
||||
// The 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.
|
||||
padding-left: calc((var(--indent-factor) - 1) * 10px + 5px);
|
||||
text-indent: -5px;
|
||||
padding-left: calc((var(--indent-factor) - 1) * 0.714em + 0.357em);
|
||||
text-indent: -0.357em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,13 @@
|
||||
|
||||
import { CopyButton } from "@/app/element/copybutton";
|
||||
import { createContentBlockPlugin } from "@/app/element/markdown-contentblock-plugin";
|
||||
import { MarkdownContentBlockType, transformBlocks } from "@/app/element/markdown-util";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { getWebServerEndpoint } from "@/util/endpoints";
|
||||
import { isBlank, makeConnRoute, useAtomValueSafe } from "@/util/util";
|
||||
import {
|
||||
MarkdownContentBlockType,
|
||||
resolveRemoteFile,
|
||||
resolveSrcSet,
|
||||
transformBlocks,
|
||||
} from "@/app/element/markdown-util";
|
||||
import { boundNumber, useAtomValueSafe } from "@/util/util";
|
||||
import { clsx } from "clsx";
|
||||
import { Atom } from "jotai";
|
||||
import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
|
||||
@ -108,8 +110,34 @@ const CodeBlock = ({ children, onClickExecute }: CodeBlockProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const MarkdownSource = (props: React.HTMLAttributes<HTMLSourceElement>) => {
|
||||
return null;
|
||||
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 <source srcSet={resolvedSrcSet} media={props.media} />;
|
||||
};
|
||||
|
||||
interface WaveBlockProps {
|
||||
@ -148,16 +176,11 @@ const MarkdownImg = ({
|
||||
resolveOpts: MarkdownResolveOpts;
|
||||
}) => {
|
||||
const [resolvedSrc, setResolvedSrc] = useState<string>(props.src);
|
||||
const [resolvedSrcSet, setResolvedSrcSet] = useState<string>(props.srcSet);
|
||||
const [resolvedStr, setResolvedStr] = useState<string>(null);
|
||||
const [resolving, setResolving] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.src.startsWith("http://") || props.src.startsWith("https://")) {
|
||||
setResolving(false);
|
||||
setResolvedSrc(props.src);
|
||||
setResolvedStr(null);
|
||||
return;
|
||||
}
|
||||
if (props.src.startsWith("data:image/")) {
|
||||
setResolving(false);
|
||||
setResolvedSrc(props.src);
|
||||
@ -170,23 +193,20 @@ const MarkdownImg = ({
|
||||
setResolvedStr(`[img:${props.src}]`);
|
||||
return;
|
||||
}
|
||||
|
||||
const resolveFn = async () => {
|
||||
const route = makeConnRoute(resolveOpts.connName);
|
||||
const fileInfo = await RpcApi.RemoteFileJoinCommand(TabRpcClient, [resolveOpts.baseDir, props.src], {
|
||||
route: route,
|
||||
});
|
||||
const usp = new URLSearchParams();
|
||||
usp.set("path", fileInfo.path);
|
||||
if (!isBlank(resolveOpts.connName)) {
|
||||
usp.set("connection", resolveOpts.connName);
|
||||
}
|
||||
const streamingUrl = getWebServerEndpoint() + "/wave/stream-file?" + usp.toString();
|
||||
setResolvedSrc(streamingUrl);
|
||||
const [resolvedSrc, resolvedSrcSet] = await Promise.all([
|
||||
resolveRemoteFile(props.src, resolveOpts),
|
||||
resolveSrcSet(props.srcSet, resolveOpts),
|
||||
]);
|
||||
|
||||
setResolvedSrc(resolvedSrc);
|
||||
setResolvedSrcSet(resolvedSrcSet);
|
||||
setResolvedStr(null);
|
||||
setResolving(false);
|
||||
};
|
||||
resolveFn();
|
||||
}, [props.src]);
|
||||
}, [props.src, props.srcSet]);
|
||||
|
||||
if (resolving) {
|
||||
return null;
|
||||
@ -195,7 +215,7 @@ const MarkdownImg = ({
|
||||
return <span>{resolvedStr}</span>;
|
||||
}
|
||||
if (resolvedSrc != null) {
|
||||
return <img {...props} src={resolvedSrc} />;
|
||||
return <img {...props} src={resolvedSrc} srcSet={resolvedSrcSet} />;
|
||||
}
|
||||
return <span>[img]</span>;
|
||||
};
|
||||
@ -210,6 +230,8 @@ type MarkdownProps = {
|
||||
resolveOpts?: MarkdownResolveOpts;
|
||||
scrollable?: boolean;
|
||||
rehype?: boolean;
|
||||
fontSizeOverride?: number;
|
||||
fixedFontSizeOverride?: number;
|
||||
};
|
||||
|
||||
const Markdown = ({
|
||||
@ -219,6 +241,8 @@ const Markdown = ({
|
||||
style,
|
||||
className,
|
||||
resolveOpts,
|
||||
fontSizeOverride,
|
||||
fixedFontSizeOverride,
|
||||
scrollable = true,
|
||||
rehype = true,
|
||||
onClickExecute,
|
||||
@ -262,7 +286,9 @@ const Markdown = ({
|
||||
h5: (props: React.HTMLAttributes<HTMLHeadingElement>) => <Heading props={props} hnum={5} />,
|
||||
h6: (props: React.HTMLAttributes<HTMLHeadingElement>) => <Heading props={props} hnum={6} />,
|
||||
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,
|
||||
pre: (props: React.HTMLAttributes<HTMLPreElement>) => (
|
||||
<CodeBlock children={props.children} onClickExecute={onClickExecute} />
|
||||
@ -301,12 +327,15 @@ const Markdown = ({
|
||||
...(defaultSchema.attributes?.span || []),
|
||||
// Allow all class names starting with `hljs-`.
|
||||
["className", /^hljs-./],
|
||||
["srcset"],
|
||||
["media"],
|
||||
["type"],
|
||||
// Alternatively, to allow only certain class names:
|
||||
// ['className', 'hljs-number', 'hljs-title', 'hljs-variable']
|
||||
],
|
||||
waveblock: [["blockkey"]],
|
||||
},
|
||||
tagNames: [...(defaultSchema.tagNames || []), "span", "waveblock"],
|
||||
tagNames: [...(defaultSchema.tagNames || []), "span", "waveblock", "picture", "source"],
|
||||
}),
|
||||
() => 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 (
|
||||
<div className={clsx("markdown", className)} style={style}>
|
||||
<div className={clsx("markdown", className)} style={mergedStyle}>
|
||||
{scrollable ? <ScrollableMarkdown /> : <NonScrollableMarkdown />}
|
||||
{toc && (
|
||||
<OverlayScrollbarsComponent className="toc" options={{ scrollbars: { autoHide: "leave" } }}>
|
||||
|
@ -2,7 +2,7 @@
|
||||
overflow: hidden;
|
||||
.menu-anchor {
|
||||
width: 100%;
|
||||
.button {
|
||||
.wave-button {
|
||||
width: 100%;
|
||||
div {
|
||||
max-width: 100%;
|
||||
|
@ -48,7 +48,7 @@
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
|
||||
.button {
|
||||
.wave-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
|
@ -65,7 +65,7 @@
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
|
||||
.button:last-child {
|
||||
.wave-button:last-child {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
@ -230,6 +230,7 @@
|
||||
justify-content: center;
|
||||
|
||||
button {
|
||||
padding: 8px 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
@ -202,6 +202,11 @@ class RpcApiType {
|
||||
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]
|
||||
RemoteFileDeleteCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<void> {
|
||||
return client.wshRpcCall("remotefiledelete", data, opts);
|
||||
|
@ -81,7 +81,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
.wave-button {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 4px;
|
||||
|
@ -1,4 +1,4 @@
|
||||
.button {
|
||||
.wave-button {
|
||||
color: black;
|
||||
background-color: var(--accent-color);
|
||||
flex: 0 0 fit-content;
|
||||
|
70
frontend/app/tab/workspaceeditor.scss
Normal file
70
frontend/app/tab/workspaceeditor.scss
Normal 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;
|
||||
}
|
||||
}
|
125
frontend/app/tab/workspaceeditor.tsx
Normal file
125
frontend/app/tab/workspaceeditor.tsx
Normal 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;
|
@ -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 {
|
||||
min-height: auto;
|
||||
display: flex;
|
||||
@ -55,6 +36,25 @@
|
||||
border-radius: 8px;
|
||||
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 {
|
||||
font-size: 12px;
|
||||
line-height: 19px;
|
||||
@ -100,17 +100,17 @@
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.iconbutton.edit {
|
||||
.wave-iconbutton.edit {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.iconbutton.window {
|
||||
.wave-iconbutton.window {
|
||||
cursor: default;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .iconbutton.edit {
|
||||
&:hover .wave-iconbutton.edit {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
@ -144,83 +144,6 @@
|
||||
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 {
|
||||
width: 100%;
|
||||
padding: 3px 0;
|
||||
|
@ -1,7 +1,6 @@
|
||||
// Copyright 2024, Command Line
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Button } from "@/element/button";
|
||||
import {
|
||||
ExpandableMenu,
|
||||
ExpandableMenuItem,
|
||||
@ -10,139 +9,22 @@ import {
|
||||
ExpandableMenuItemLeftElement,
|
||||
ExpandableMenuItemRightElement,
|
||||
} from "@/element/expandablemenu";
|
||||
import { Input } from "@/element/input";
|
||||
import { Popover, PopoverButton, PopoverContent } from "@/element/popover";
|
||||
import { fireAndForget, makeIconClass, useAtomValueSafe } from "@/util/util";
|
||||
import clsx from "clsx";
|
||||
import { atom, PrimitiveAtom, useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||
import { splitAtom } from "jotai/utils";
|
||||
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 { IconButton } from "../element/iconbutton";
|
||||
import { atoms, getApi } from "../store/global";
|
||||
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";
|
||||
|
||||
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 = {
|
||||
windowId: string;
|
||||
workspace: Workspace;
|
||||
@ -175,15 +57,21 @@ const WorkspaceSwitcher = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
setWorkspaceList(newList);
|
||||
}, []);
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
waveEventSubscribe({
|
||||
eventType: "workspace:update",
|
||||
handler: () => fireAndForget(updateWorkspaceList),
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fireAndForget(updateWorkspaceList);
|
||||
}, []);
|
||||
|
||||
const onDeleteWorkspace = useCallback((workspaceId: string) => {
|
||||
getApi().deleteWorkspace(workspaceId);
|
||||
setTimeout(() => {
|
||||
fireAndForget(updateWorkspaceList);
|
||||
}, 10);
|
||||
}, []);
|
||||
|
||||
const isActiveWorkspaceSaved = !!(activeWorkspace.name && activeWorkspace.icon);
|
||||
@ -266,9 +154,16 @@ const WorkspaceSwitcherItem = ({
|
||||
|
||||
const setWorkspace = useCallback((newWorkspace: Workspace) => {
|
||||
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;
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Used for syntax highlighting in markdown
|
||||
|
||||
:root {
|
||||
--main-text-color: #f7f7f7;
|
||||
--title-font-size: 18px;
|
||||
@ -16,8 +14,10 @@
|
||||
--accent-color: rgb(88, 193, 66);
|
||||
--panel-bg-color: rgba(31, 33, 31, 0.5);
|
||||
--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";
|
||||
--markdown-font-size: 14px;
|
||||
--markdown-fixed-font-size: 12px;
|
||||
--error-color: rgb(229, 77, 46);
|
||||
--warning-color: rgb(224, 185, 86);
|
||||
--success-color: rgb(78, 154, 6);
|
||||
|
@ -122,6 +122,7 @@ export function CodeEditor({ blockId, text, language, filename, meta, onChange,
|
||||
const minimapEnabled = useOverrideConfigAtom(blockId, "editor:minimapenabled") ?? false;
|
||||
const stickyScrollEnabled = useOverrideConfigAtom(blockId, "editor:stickyscrollenabled") ?? false;
|
||||
const wordWrap = useOverrideConfigAtom(blockId, "editor:wordwrap") ?? false;
|
||||
const fontSize = useOverrideConfigAtom(blockId, "editor:fontsize");
|
||||
const theme = "wave-theme-dark";
|
||||
|
||||
React.useEffect(() => {
|
||||
@ -150,8 +151,9 @@ export function CodeEditor({ blockId, text, language, filename, meta, onChange,
|
||||
opts.minimap.enabled = minimapEnabled;
|
||||
opts.stickyScroll.enabled = stickyScrollEnabled;
|
||||
opts.wordWrap = wordWrap ? "on" : "off";
|
||||
opts.fontSize = fontSize;
|
||||
return opts;
|
||||
}, [minimapEnabled, stickyScrollEnabled, wordWrap]);
|
||||
}, [minimapEnabled, stickyScrollEnabled, wordWrap, fontSize]);
|
||||
|
||||
return (
|
||||
<div className="code-editor-wrapper">
|
||||
|
@ -783,6 +783,8 @@ function makePreviewModel(blockId: string, nodeModel: BlockNodeModel): PreviewMo
|
||||
function MarkdownPreview({ model }: SpecializedViewProps) {
|
||||
const connName = useAtomValue(model.connection);
|
||||
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>(() => {
|
||||
return {
|
||||
connName: connName,
|
||||
@ -791,7 +793,13 @@ function MarkdownPreview({ model }: SpecializedViewProps) {
|
||||
}, [connName, fileInfo.dir]);
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import {
|
||||
} from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import * as keyutil from "@/util/keyutil";
|
||||
import { boundNumber } from "@/util/util";
|
||||
import clsx from "clsx";
|
||||
import debug from "debug";
|
||||
import * as jotai from "jotai";
|
||||
@ -62,6 +63,7 @@ class TermViewModel implements ViewModel {
|
||||
vdomToolbarTarget: jotai.PrimitiveAtom<VDomTargetToolbar>;
|
||||
fontSizeAtom: jotai.Atom<number>;
|
||||
termThemeNameAtom: jotai.Atom<string>;
|
||||
termTransparencyAtom: jotai.Atom<number>;
|
||||
noPadding: jotai.PrimitiveAtom<boolean>;
|
||||
endIconButtons: jotai.Atom<IconButtonDecl[]>;
|
||||
shellProcFullStatus: jotai.PrimitiveAtom<BlockControllerRuntimeStatus>;
|
||||
@ -203,10 +205,17 @@ class TermViewModel implements ViewModel {
|
||||
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) => {
|
||||
const fullConfig = get(atoms.fullConfigAtom);
|
||||
const themeName = get(this.termThemeNameAtom);
|
||||
const [_, bgcolor] = computeTheme(fullConfig, themeName);
|
||||
const termTransparency = get(this.termTransparencyAtom);
|
||||
const [_, bgcolor] = computeTheme(fullConfig, themeName, termTransparency);
|
||||
if (bgcolor != null) {
|
||||
return { bg: bgcolor };
|
||||
}
|
||||
@ -407,6 +416,11 @@ class TermViewModel implements ViewModel {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
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);
|
||||
if ((shellProcStatus == "done" || shellProcStatus == "init") && keyutil.checkKeyPressed(waveEvent, "Enter")) {
|
||||
@ -453,6 +467,7 @@ class TermViewModel implements ViewModel {
|
||||
const termThemeKeys = Object.keys(termThemes);
|
||||
const curThemeName = globalStore.get(getBlockMetaKeyAtom(this.blockId, "term:theme"));
|
||||
const defaultFontSize = globalStore.get(getSettingsKeyAtom("term:fontsize")) ?? 12;
|
||||
const transparencyMeta = globalStore.get(getBlockMetaKeyAtom(this.blockId, "term:transparency"));
|
||||
const blockData = globalStore.get(this.blockAtom);
|
||||
const overrideFontSize = blockData?.meta?.["term:fontsize"];
|
||||
|
||||
@ -474,6 +489,41 @@ class TermViewModel implements ViewModel {
|
||||
checked: curThemeName == 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(
|
||||
(fontSize: number) => {
|
||||
return {
|
||||
@ -508,6 +558,10 @@ class TermViewModel implements ViewModel {
|
||||
label: "Font Size",
|
||||
submenu: fontSizeSubMenu,
|
||||
});
|
||||
fullMenu.push({
|
||||
label: "Transparency",
|
||||
submenu: transparencySubMenu,
|
||||
});
|
||||
fullMenu.push({ type: "separator" });
|
||||
fullMenu.push({
|
||||
label: "Force Restart Controller",
|
||||
@ -734,7 +788,8 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
||||
React.useEffect(() => {
|
||||
const fullConfig = globalStore.get(atoms.fullConfigAtom);
|
||||
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;
|
||||
if (termSettings?.["term:scrollback"]) {
|
||||
termScrollback = Math.floor(termSettings["term:scrollback"]);
|
||||
|
@ -17,7 +17,8 @@ interface TermThemeProps {
|
||||
const TermThemeUpdater = ({ blockId, model, termRef }: TermThemeProps) => {
|
||||
const fullConfig = useAtomValue(atoms.fullConfigAtom);
|
||||
const blockTermTheme = useAtomValue(model.termThemeNameAtom);
|
||||
const [theme, _] = computeTheme(fullConfig, blockTermTheme);
|
||||
const transparency = useAtomValue(model.termTransparencyAtom);
|
||||
const [theme, _] = computeTheme(fullConfig, blockTermTheme, transparency);
|
||||
useEffect(() => {
|
||||
if (termRef.current?.terminal) {
|
||||
termRef.current.terminal.options.theme = theme;
|
||||
|
@ -2,14 +2,32 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export const DefaultTermTheme = "default-dark";
|
||||
import { colord } from "colord";
|
||||
|
||||
// returns (theme, bgcolor)
|
||||
function computeTheme(fullConfig: FullConfigType, themeName: string): [TermThemeType, string] {
|
||||
function applyTransparencyToColor(hexColor: string, transparency: number): 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];
|
||||
if (theme == null) {
|
||||
theme = fullConfig?.termthemes?.[DefaultTermTheme] || ({} as any);
|
||||
}
|
||||
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;
|
||||
themeCopy.background = "#00000000";
|
||||
return [themeCopy, bgcolor];
|
||||
|
@ -165,6 +165,9 @@ export class TermWrap {
|
||||
}
|
||||
|
||||
handleTermData(data: string) {
|
||||
if (!this.loaded) {
|
||||
return;
|
||||
}
|
||||
const b64data = util.stringToBase64(data);
|
||||
RpcApi.ControllerInputCommand(TabRpcClient, { blockid: this.blockId, inputdata64: b64data });
|
||||
}
|
||||
|
@ -131,9 +131,6 @@
|
||||
outline: none;
|
||||
overflow: auto;
|
||||
overflow-wrap: anywhere;
|
||||
font-family: var(--termfontfamily);
|
||||
font-weight: normal;
|
||||
line-height: var(--termlineheight);
|
||||
height: 21px;
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import { RpcResponseHelper, WshClient } from "@/app/store/wshclient";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { makeFeBlockRouteId } from "@/app/store/wshrouter";
|
||||
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 { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
|
||||
import { fireAndForget, isBlank, makeIconClass } from "@/util/util";
|
||||
@ -30,6 +30,7 @@ const slidingWindowSize = 30;
|
||||
|
||||
interface ChatItemProps {
|
||||
chatItem: ChatMessageType;
|
||||
model: WaveAiModel;
|
||||
}
|
||||
|
||||
function promptToMsg(prompt: OpenAIPromptMessageType): ChatMessageType {
|
||||
@ -430,11 +431,12 @@ function makeWaveAiViewModel(blockId: string): WaveAiModel {
|
||||
return waveAiModel;
|
||||
}
|
||||
|
||||
const ChatItem = ({ chatItem }: ChatItemProps) => {
|
||||
const ChatItem = ({ chatItem, model }: ChatItemProps) => {
|
||||
const { user, text } = chatItem;
|
||||
const cssVar = "--panel-bg-color";
|
||||
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(() => {
|
||||
if (user == "error") {
|
||||
return (
|
||||
@ -445,7 +447,12 @@ const ChatItem = ({ chatItem }: ChatItemProps) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="chat-msg chat-msg-error">
|
||||
<Markdown text={text} scrollable={false} />
|
||||
<Markdown
|
||||
text={text}
|
||||
scrollable={false}
|
||||
fontSizeOverride={fontSize}
|
||||
fixedFontSizeOverride={fixedFontSize}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@ -459,7 +466,12 @@ const ChatItem = ({ chatItem }: ChatItemProps) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="chat-msg chat-msg-assistant">
|
||||
<Markdown text={text} scrollable={false} />
|
||||
<Markdown
|
||||
text={text}
|
||||
scrollable={false}
|
||||
fontSizeOverride={fontSize}
|
||||
fixedFontSizeOverride={fixedFontSize}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
@ -474,11 +486,17 @@ const ChatItem = ({ chatItem }: ChatItemProps) => {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}, [text, user]);
|
||||
}, [text, user, fontSize, fixedFontSize]);
|
||||
|
||||
return <div className={"chat-msg-container"}>{renderContent}</div>;
|
||||
};
|
||||
@ -487,10 +505,11 @@ interface ChatWindowProps {
|
||||
chatWindowRef: React.RefObject<HTMLDivElement>;
|
||||
messages: ChatMessageType[];
|
||||
msgWidths: Object;
|
||||
model: WaveAiModel;
|
||||
}
|
||||
|
||||
const ChatWindow = memo(
|
||||
forwardRef<OverlayScrollbarsComponentRef, ChatWindowProps>(({ chatWindowRef, messages, msgWidths }, ref) => {
|
||||
forwardRef<OverlayScrollbarsComponentRef, ChatWindowProps>(({ chatWindowRef, messages, msgWidths, model }, ref) => {
|
||||
const [isUserScrolling, setIsUserScrolling] = useState(false);
|
||||
|
||||
const osRef = useRef<OverlayScrollbarsComponentRef>(null);
|
||||
@ -559,7 +578,7 @@ const ChatWindow = memo(
|
||||
<div ref={chatWindowRef} className="chat-window" style={msgWidths}>
|
||||
<div className="filler"></div>
|
||||
{messages.map((chitem, idx) => (
|
||||
<ChatItem key={idx} chatItem={chitem} />
|
||||
<ChatItem key={idx} chatItem={chitem} model={model} />
|
||||
))}
|
||||
</div>
|
||||
</OverlayScrollbarsComponent>
|
||||
@ -569,7 +588,7 @@ const ChatWindow = memo(
|
||||
|
||||
interface ChatInputProps {
|
||||
value: string;
|
||||
termFontSize: number;
|
||||
baseFontSize: number;
|
||||
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||
onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
onMouseDown: (e: React.MouseEvent<HTMLTextAreaElement>) => void;
|
||||
@ -577,7 +596,7 @@ interface 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);
|
||||
|
||||
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
|
||||
const textAreaMaxLines = 5;
|
||||
const textAreaLineHeight = termFontSize * 1.5;
|
||||
const textAreaLineHeight = baseFontSize * 1.5;
|
||||
const textAreaMinHeight = textAreaLineHeight;
|
||||
const textAreaMaxHeight = textAreaLineHeight * textAreaMaxLines;
|
||||
|
||||
@ -608,7 +627,7 @@ const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>(
|
||||
const newHeight = Math.min(Math.max(scrollHeight, textAreaMinHeight), textAreaMaxHeight);
|
||||
textAreaRef.current.style.height = newHeight + "px";
|
||||
},
|
||||
[termFontSize]
|
||||
[baseFontSize]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -624,7 +643,7 @@ const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>(
|
||||
onMouseDown={onMouseDown} // When the user clicks on the textarea
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
style={{ fontSize: termFontSize }}
|
||||
style={{ fontSize: baseFontSize }}
|
||||
placeholder="Ask anything..."
|
||||
value={value}
|
||||
></textarea>
|
||||
@ -642,7 +661,7 @@ const WaveAi = ({ model }: { model: WaveAiModel; blockId: string }) => {
|
||||
const [value, setValue] = useState("");
|
||||
const [selectedBlockIdx, setSelectedBlockIdx] = useState<number | null>(null);
|
||||
|
||||
const termFontSize: number = 14;
|
||||
const baseFontSize: number = 14;
|
||||
const msgWidths = {};
|
||||
const locked = useAtomValue(model.locked);
|
||||
|
||||
@ -804,7 +823,13 @@ const WaveAi = ({ model }: { model: WaveAiModel; blockId: string }) => {
|
||||
return (
|
||||
<div ref={waveaiRef} className="waveai">
|
||||
<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 className="waveai-controls">
|
||||
<div className="waveai-input-wrapper">
|
||||
@ -815,7 +840,7 @@ const WaveAi = ({ model }: { model: WaveAiModel; blockId: string }) => {
|
||||
onChange={handleTextAreaChange}
|
||||
onKeyDown={handleTextAreaKeyDown}
|
||||
onMouseDown={handleTextAreaMouseDown}
|
||||
termFontSize={termFontSize}
|
||||
baseFontSize={baseFontSize}
|
||||
/>
|
||||
</div>
|
||||
<Button className={buttonClass} onClick={handleButtonPress}>
|
||||
|
@ -42,7 +42,7 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.iconbutton {
|
||||
.wave-iconbutton {
|
||||
width: fit-content !important;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
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 { ObjectService } from "@/app/store/services";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
@ -49,13 +49,13 @@ export class WebViewModel implements ViewModel {
|
||||
mediaPlaying: PrimitiveAtom<boolean>;
|
||||
mediaMuted: PrimitiveAtom<boolean>;
|
||||
modifyExternalUrl?: (url: string) => string;
|
||||
domReady: PrimitiveAtom<boolean>;
|
||||
|
||||
constructor(blockId: string, nodeModel: BlockNodeModel) {
|
||||
this.nodeModel = nodeModel;
|
||||
this.viewType = "web";
|
||||
this.blockId = blockId;
|
||||
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
|
||||
|
||||
this.url = atom();
|
||||
const defaultUrlAtom = getSettingsKeyAtom("web:defaulturl");
|
||||
this.homepageUrl = atom((get) => {
|
||||
@ -71,6 +71,7 @@ export class WebViewModel implements ViewModel {
|
||||
this.viewName = atom("Web");
|
||||
this.urlInputRef = createRef<HTMLInputElement>();
|
||||
this.webviewRef = createRef<WebviewTag>();
|
||||
this.domReady = atom(false);
|
||||
|
||||
this.mediaPlaying = atom(false);
|
||||
this.mediaMuted = atom(false);
|
||||
@ -339,7 +340,7 @@ export class WebViewModel implements ViewModel {
|
||||
const defaultSearchAtom = getSettingsKeyAtom("web:defaultsearch");
|
||||
const searchTemplate = globalStore.get(defaultSearchAtom);
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
@ -414,7 +415,7 @@ export class WebViewModel implements ViewModel {
|
||||
return true;
|
||||
}
|
||||
if (checkKeyPressed(e, "Cmd:r")) {
|
||||
this.webviewRef?.current?.reload();
|
||||
this.webviewRef.current?.reload();
|
||||
return true;
|
||||
}
|
||||
if (checkKeyPressed(e, "Cmd:ArrowLeft")) {
|
||||
@ -428,7 +429,61 @@ export class WebViewModel implements ViewModel {
|
||||
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[] {
|
||||
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 [
|
||||
{
|
||||
label: "Set Block Homepage",
|
||||
@ -441,6 +496,10 @@ export class WebViewModel implements ViewModel {
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "Set Zoom Factor",
|
||||
submenu: zoomSubMenu,
|
||||
},
|
||||
{
|
||||
label: this.webviewRef.current?.isDevToolsOpened() ? "Close DevTools" : "Open DevTools",
|
||||
click: () => {
|
||||
@ -476,12 +535,13 @@ const WebView = memo(({ model, onFailLoad }: WebViewProps) => {
|
||||
let metaUrl = blockData?.meta?.url || defaultUrl;
|
||||
metaUrl = model.ensureUrlScheme(metaUrl, defaultSearch);
|
||||
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.
|
||||
const [metaUrlInitial] = useState(metaUrl);
|
||||
|
||||
const [webContentsId, setWebContentsId] = useState(null);
|
||||
const [domReady, setDomReady] = useState(false);
|
||||
const domReady = useAtomValue(model.domReady);
|
||||
|
||||
const [errorText, setErrorText] = useState("");
|
||||
|
||||
@ -510,13 +570,27 @@ const WebView = memo(({ model, onFailLoad }: WebViewProps) => {
|
||||
}
|
||||
|
||||
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?.();
|
||||
if (wcId) {
|
||||
setWebContentsId(wcId);
|
||||
if (model.webviewRef.current.getZoomFactor() != zoomFactor) {
|
||||
model.webviewRef.current.setZoomFactor(zoomFactor);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to get webcontentsid / setzoomlevel (webview)", e);
|
||||
}
|
||||
}, [model.webviewRef.current, domReady]);
|
||||
}, [model.webviewRef.current, domReady, zoomFactor]);
|
||||
|
||||
// Load a new URL if the block metadata is updated.
|
||||
useEffect(() => {
|
||||
@ -560,7 +634,7 @@ const WebView = memo(({ model, onFailLoad }: WebViewProps) => {
|
||||
console.error(errorMessage);
|
||||
setErrorText(errorMessage);
|
||||
if (onFailLoad) {
|
||||
const curUrl = model.webviewRef?.current.getURL();
|
||||
const curUrl = model.webviewRef.current.getURL();
|
||||
onFailLoad(curUrl);
|
||||
}
|
||||
}
|
||||
@ -573,7 +647,7 @@ const WebView = memo(({ model, onFailLoad }: WebViewProps) => {
|
||||
getApi().setWebviewFocus(null);
|
||||
};
|
||||
const handleDomReady = () => {
|
||||
setDomReady(true);
|
||||
globalStore.set(model.domReady, true);
|
||||
setBgColor();
|
||||
};
|
||||
const handleMediaPlaying = () => {
|
||||
|
@ -412,6 +412,11 @@ export class LayoutModel {
|
||||
for (const action of actions) {
|
||||
switch (action.actiontype) {
|
||||
case LayoutTreeActionType.InsertNode: {
|
||||
if (action.ephemeral) {
|
||||
this.newEphemeralNode(action.blockid);
|
||||
break;
|
||||
}
|
||||
|
||||
const insertNodeAction: LayoutTreeInsertNodeAction = {
|
||||
type: LayoutTreeActionType.InsertNode,
|
||||
node: newLayoutNode(undefined, undefined, undefined, {
|
||||
|
22
frontend/types/gotypes.d.ts
vendored
22
frontend/types/gotypes.d.ts
vendored
@ -401,6 +401,7 @@ declare global {
|
||||
indexarr?: number[];
|
||||
focused: boolean;
|
||||
magnified: boolean;
|
||||
ephemeral: boolean;
|
||||
};
|
||||
|
||||
// waveobj.LayoutState
|
||||
@ -490,6 +491,10 @@ declare global {
|
||||
"term:scrollback"?: number;
|
||||
"term:vdomblockid"?: string;
|
||||
"term:vdomtoolbarblockid"?: string;
|
||||
"term:transparency"?: number;
|
||||
"web:zoom"?: number;
|
||||
"markdown:fontsize"?: number;
|
||||
"markdown:fixedfontsize"?: number;
|
||||
"vdom:*"?: boolean;
|
||||
"vdom:initialized"?: boolean;
|
||||
"vdom:correlationid"?: string;
|
||||
@ -560,6 +565,14 @@ declare global {
|
||||
prompt: OpenAIPromptMessageType[];
|
||||
};
|
||||
|
||||
// wshrpc.PathCommandData
|
||||
type PathCommandData = {
|
||||
pathtype: string;
|
||||
open: boolean;
|
||||
openexternal: boolean;
|
||||
tabid: string;
|
||||
};
|
||||
|
||||
// waveobj.Point
|
||||
type Point = {
|
||||
x: number;
|
||||
@ -604,6 +617,9 @@ declare global {
|
||||
|
||||
// wconfig.SettingsType
|
||||
type SettingsType = {
|
||||
"app:*"?: boolean;
|
||||
"app:globalhotkey"?: string;
|
||||
"app:dismissarchitecturewarning"?: boolean;
|
||||
"ai:*"?: boolean;
|
||||
"ai:preset"?: string;
|
||||
"ai:apitype"?: string;
|
||||
@ -615,6 +631,8 @@ declare global {
|
||||
"ai:apiversion"?: string;
|
||||
"ai:maxtokens"?: number;
|
||||
"ai:timeoutms"?: number;
|
||||
"ai:fontsize"?: number;
|
||||
"ai:fixedfontsize"?: number;
|
||||
"term:*"?: boolean;
|
||||
"term:fontsize"?: number;
|
||||
"term:fontfamily"?: string;
|
||||
@ -624,9 +642,11 @@ declare global {
|
||||
"term:localshellopts"?: string[];
|
||||
"term:scrollback"?: number;
|
||||
"term:copyonselect"?: boolean;
|
||||
"term:transparency"?: number;
|
||||
"editor:minimapenabled"?: boolean;
|
||||
"editor:stickyscrollenabled"?: boolean;
|
||||
"editor:wordwrap"?: boolean;
|
||||
"editor:fontsize"?: number;
|
||||
"web:*"?: boolean;
|
||||
"web:openlinksinternally"?: boolean;
|
||||
"web:defaulturl"?: string;
|
||||
@ -638,6 +658,8 @@ declare global {
|
||||
"autoupdate:intervalms"?: number;
|
||||
"autoupdate:installonquit"?: boolean;
|
||||
"autoupdate:channel"?: string;
|
||||
"markdown:fontsize"?: number;
|
||||
"markdown:fixedfontsize"?: number;
|
||||
"preview:showhiddenfiles"?: boolean;
|
||||
"tab:preset"?: string;
|
||||
"widget:*"?: boolean;
|
||||
|
5
go.mod
5
go.mod
@ -18,8 +18,9 @@ require (
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/sashabaranov/go-openai v1.36.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/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/ubuntu/gowsl v0.0.0-20240906163211-049fd49bd93b
|
||||
github.com/wavetermdev/htmltoken v0.2.0
|
||||
@ -47,6 +48,6 @@ require (
|
||||
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
|
||||
|
10
go.sum
10
go.sum
@ -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/sawka/txwrap v0.2.0 h1:V3LfvKVLULxcYSxdMguLwFyQFMEU9nFDJopg0ZkL+94=
|
||||
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.10/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8=
|
||||
github.com/shirou/gopsutil/v4 v4.24.11 h1:WaU9xqGFKvFfsUv94SXcUPD7rCkU0vr/asVdQOBZNj8=
|
||||
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/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
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/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/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
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/wavetermdev/htmltoken v0.2.0 h1:sFVPPemlDv7/jg7n4Hx1AEF2m9MVAFjFpELWfhi/DlM=
|
||||
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-20241027232332-ed124367682d/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||
github.com/wavetermdev/ssh_config v0.0.0-20241219203747-6409e4292f34 h1:I8VZVTZEXhnzfN7jB9a7TZYpzNO48sCUWMRXHM9XWSA=
|
||||
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/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||
|
15
package.json
15
package.json
@ -7,7 +7,7 @@
|
||||
"productName": "Wave",
|
||||
"description": "Open-Source AI-Native Terminal Built for Seamless Workflows",
|
||||
"license": "Apache-2.0",
|
||||
"version": "0.10.1",
|
||||
"version": "0.10.2-beta.2",
|
||||
"homepage": "https://waveterm.dev",
|
||||
"build": {
|
||||
"appId": "dev.commandline.waveterm"
|
||||
@ -43,7 +43,7 @@
|
||||
"@types/css-tree": "^2",
|
||||
"@types/debug": "^4",
|
||||
"@types/electron": "^1.6.12",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/papaparse": "^5",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
"@types/prop-types": "^15",
|
||||
@ -58,7 +58,7 @@
|
||||
"@types/ws": "^8",
|
||||
"@vitejs/plugin-react-swc": "^3.7.2",
|
||||
"@vitest/coverage-istanbul": "^2.1.8",
|
||||
"electron": "^33.2.0",
|
||||
"electron": "^33.2.1",
|
||||
"electron-builder": "^25.1.8",
|
||||
"electron-vite": "^2.3.0",
|
||||
"eslint": "^9.16.0",
|
||||
@ -67,7 +67,7 @@
|
||||
"prettier-plugin-jsdoc": "^1.3.0",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"rollup-plugin-flow": "^1.1.1",
|
||||
"sass": "^1.82.0",
|
||||
"sass": "^1.83.0",
|
||||
"semver": "^7.6.3",
|
||||
"storybook": "^8.4.7",
|
||||
"storybook-dark-mode": "^4.0.2",
|
||||
@ -75,12 +75,12 @@
|
||||
"tslib": "^2.8.1",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.17.0",
|
||||
"vite": "^6.0.2",
|
||||
"typescript-eslint": "^8.18.0",
|
||||
"vite": "^6.0.3",
|
||||
"vite-plugin-image-optimizer": "^1.1.8",
|
||||
"vite-plugin-static-copy": "^2.2.0",
|
||||
"vite-plugin-svgr": "^4.3.0",
|
||||
"vite-tsconfig-paths": "^5.1.3",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^2.1.8"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -116,6 +116,7 @@
|
||||
"overlayscrollbars": "^2.10.1",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"papaparse": "^5.4.1",
|
||||
"parse-srcset": "^1.0.2",
|
||||
"pngjs": "^7.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.3.1",
|
||||
|
@ -227,6 +227,9 @@ func (entry *CacheEntry) readAt(ctx context.Context, offset int64, size int64, r
|
||||
offset += truncateAmt
|
||||
size -= truncateAmt
|
||||
}
|
||||
if size <= 0 {
|
||||
return realDataOffset, nil, nil
|
||||
}
|
||||
}
|
||||
partMap := file.computePartMap(offset, size)
|
||||
dataEntryMap, err := entry.loadDataPartsForRead(ctx, getPartIdxsFromMap(partMap))
|
||||
|
@ -509,11 +509,6 @@ func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.Conn
|
||||
conn.WithLock(func() {
|
||||
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()
|
||||
enableWsh := config.Settings.ConnWshEnabled
|
||||
askBeforeInstall := config.Settings.ConnAskBeforeWshInstall
|
||||
@ -545,15 +540,22 @@ func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.Conn
|
||||
}
|
||||
|
||||
if conn.WshEnabled.Load() {
|
||||
csErr := conn.StartConnServer()
|
||||
if csErr != nil {
|
||||
log.Printf("error: unable to start conn server for %s: %v\n", conn.GetName(), csErr)
|
||||
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 {
|
||||
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")
|
||||
conn.WithLock(func() {
|
||||
conn.WshError = csErr.Error()
|
||||
})
|
||||
conn.WshEnabled.Store(false)
|
||||
//return fmt.Errorf("conncontroller %s start wsh connserver error: %v", conn.GetName(), csErr)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -23,6 +23,7 @@ import (
|
||||
|
||||
"github.com/kevinburke/ssh_config"
|
||||
"github.com/skeema/knownhosts"
|
||||
"github.com/wavetermdev/waveterm/pkg/panichandler"
|
||||
"github.com/wavetermdev/waveterm/pkg/trimquotes"
|
||||
"github.com/wavetermdev/waveterm/pkg/userinput"
|
||||
"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
|
||||
// but `var != "no"` will default to true
|
||||
// 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()
|
||||
sshKeywords := &wshrpc.ConnKeywords{}
|
||||
var err error
|
||||
|
@ -50,6 +50,9 @@ func (svc *WorkspaceService) UpdateWorkspace(ctx context.Context, workspaceId st
|
||||
return nil, fmt.Errorf("error updating workspace: %w", err)
|
||||
}
|
||||
|
||||
wps.Broker.Publish(wps.WaveEvent{
|
||||
Event: wps.Event_WorkspaceUpdate})
|
||||
|
||||
updates := waveobj.ContextGetUpdatesRtn(ctx)
|
||||
go func() {
|
||||
defer panichandler.PanicHandler("WorkspaceService:UpdateWorkspace:SendUpdateEvents")
|
||||
|
@ -167,9 +167,6 @@ func StartWslShellProc(ctx context.Context, termSize waveobj.TermSize, cmdStr st
|
||||
homeDir := wsl.GetHomeDir(conn.Context, client)
|
||||
shellOpts = append(shellOpts, "~", "-d", client.Name())
|
||||
|
||||
if isZshShell(shellPath) {
|
||||
shellOpts = append(shellOpts, fmt.Sprintf(`ZDOTDIR="%s/.waveterm/%s"`, homeDir, shellutil.ZshIntegrationDir))
|
||||
}
|
||||
var subShellOpts []string
|
||||
|
||||
if cmdStr == "" {
|
||||
@ -216,6 +213,10 @@ func StartWslShellProc(ctx context.Context, termSize waveobj.TermSize, cmdStr st
|
||||
} else {
|
||||
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, subShellOpts...)
|
||||
log.Printf("full cmd is: %s %s", "wsl.exe", strings.Join(shellOpts, " "))
|
||||
|
@ -93,6 +93,12 @@ const (
|
||||
MetaKey_TermScrollback = "term:scrollback"
|
||||
MetaKey_TermVDomSubBlockId = "term:vdomblockid"
|
||||
MetaKey_TermVDomToolbarBlockId = "term:vdomtoolbarblockid"
|
||||
MetaKey_TermTransparency = "term:transparency"
|
||||
|
||||
MetaKey_WebZoom = "web:zoom"
|
||||
|
||||
MetaKey_MarkdownFontSize = "markdown:fontsize"
|
||||
MetaKey_MarkdownFixedFontSize = "markdown:fixedfontsize"
|
||||
|
||||
MetaKey_VDomClear = "vdom:*"
|
||||
MetaKey_VDomInitialized = "vdom:initialized"
|
||||
|
@ -208,6 +208,7 @@ type LayoutActionData struct {
|
||||
IndexArr *[]int `json:"indexarr,omitempty"`
|
||||
Focused bool `json:"focused"`
|
||||
Magnified bool `json:"magnified"`
|
||||
Ephemeral bool `json:"ephemeral"`
|
||||
}
|
||||
|
||||
type LeafOrderEntry struct {
|
||||
|
@ -94,6 +94,12 @@ type MetaTSType struct {
|
||||
TermScrollback *int `json:"term:scrollback,omitempty"`
|
||||
TermVDomSubBlockId string `json:"term:vdomblockid,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"`
|
||||
VDomInitialized bool `json:"vdom:initialized,omitempty"`
|
||||
|
@ -22,12 +22,38 @@
|
||||
"cmdtext": "#f0f0f0",
|
||||
"foreground": "#c1c1c1",
|
||||
"selectionBackground": "",
|
||||
"background": "#00000077",
|
||||
"background": "#000000",
|
||||
"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": {
|
||||
"display:name": "Dracula",
|
||||
"display:order": 2,
|
||||
"display:order": 3,
|
||||
"black": "#21222C",
|
||||
"red": "#FF5555",
|
||||
"green": "#50FA7B",
|
||||
@ -53,7 +79,7 @@
|
||||
},
|
||||
"monokai": {
|
||||
"display:name": "Monokai",
|
||||
"display:order": 3,
|
||||
"display:order": 4,
|
||||
"black": "#1B1D1E",
|
||||
"red": "#F92672",
|
||||
"green": "#A6E22E",
|
||||
@ -79,7 +105,7 @@
|
||||
},
|
||||
"campbell": {
|
||||
"display:name": "Campbell",
|
||||
"display:order": 4,
|
||||
"display:order": 5,
|
||||
"black": "#0C0C0C",
|
||||
"red": "#C50F1F",
|
||||
"green": "#13A10E",
|
||||
@ -105,7 +131,7 @@
|
||||
},
|
||||
"warmyellow": {
|
||||
"display:name": "Warm Yellow",
|
||||
"display:order": 4,
|
||||
"display:order": 6,
|
||||
"black": "#3C3228",
|
||||
"red": "#E67E22",
|
||||
"green": "#A5D6A7",
|
||||
@ -127,30 +153,30 @@
|
||||
"selectionBackground": "#B7950B",
|
||||
"cursor": "#F9D784"
|
||||
},
|
||||
"onedarkpro": {
|
||||
"display:name": "One Dark Pro",
|
||||
"display:order": 1.5,
|
||||
"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"
|
||||
"rosepine": {
|
||||
"display:name": "Rose Pine",
|
||||
"display:order": 7,
|
||||
"black": "#26233a",
|
||||
"red": "#eb6f92",
|
||||
"green": "#3e8fb0",
|
||||
"yellow": "#f6c177",
|
||||
"blue": "#9ccfd8",
|
||||
"magenta": "#c4a7e7",
|
||||
"cyan": "#ebbcba",
|
||||
"white": "#e0def4",
|
||||
"brightBlack": "#908caa",
|
||||
"brightRed": "#ff8cab",
|
||||
"brightGreen": "#9ccfb0",
|
||||
"brightYellow": "#ffd196",
|
||||
"brightBlue": "#bee6e0",
|
||||
"brightMagenta": "#e2c4ff",
|
||||
"brightCyan": "#ffd1d0",
|
||||
"brightWhite": "#fffaf3",
|
||||
"gray": "#908caa",
|
||||
"cmdtext": "#e0def4",
|
||||
"foreground": "#e0def4",
|
||||
"selectionBackground": "#403d52",
|
||||
"background": "#191724",
|
||||
"cursor": "#524f67"
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,10 @@
|
||||
package wconfig
|
||||
|
||||
const (
|
||||
ConfigKey_AppClear = "app:*"
|
||||
ConfigKey_AppGlobalHotkey = "app:globalhotkey"
|
||||
ConfigKey_AppDismissArchitectureWarning = "app:dismissarchitecturewarning"
|
||||
|
||||
ConfigKey_AiClear = "ai:*"
|
||||
ConfigKey_AiPreset = "ai:preset"
|
||||
ConfigKey_AiApiType = "ai:apitype"
|
||||
@ -17,6 +21,8 @@ const (
|
||||
ConfigKey_AIApiVersion = "ai:apiversion"
|
||||
ConfigKey_AiMaxTokens = "ai:maxtokens"
|
||||
ConfigKey_AiTimeoutMs = "ai:timeoutms"
|
||||
ConfigKey_AiFontSize = "ai:fontsize"
|
||||
ConfigKey_AiFixedFontSize = "ai:fixedfontsize"
|
||||
|
||||
ConfigKey_TermClear = "term:*"
|
||||
ConfigKey_TermFontSize = "term:fontsize"
|
||||
@ -27,10 +33,12 @@ const (
|
||||
ConfigKey_TermLocalShellOpts = "term:localshellopts"
|
||||
ConfigKey_TermScrollback = "term:scrollback"
|
||||
ConfigKey_TermCopyOnSelect = "term:copyonselect"
|
||||
ConfigKey_TermTransparency = "term:transparency"
|
||||
|
||||
ConfigKey_EditorMinimapEnabled = "editor:minimapenabled"
|
||||
ConfigKey_EditorStickyScrollEnabled = "editor:stickyscrollenabled"
|
||||
ConfigKey_EditorWordWrap = "editor:wordwrap"
|
||||
ConfigKey_EditorFontSize = "editor:fontsize"
|
||||
|
||||
ConfigKey_WebClear = "web:*"
|
||||
ConfigKey_WebOpenLinksInternally = "web:openlinksinternally"
|
||||
@ -46,6 +54,9 @@ const (
|
||||
ConfigKey_AutoUpdateInstallOnQuit = "autoupdate:installonquit"
|
||||
ConfigKey_AutoUpdateChannel = "autoupdate:channel"
|
||||
|
||||
ConfigKey_MarkdownFontSize = "markdown:fontsize"
|
||||
ConfigKey_MarkdownFixedFontSize = "markdown:fixedfontsize"
|
||||
|
||||
ConfigKey_PreviewShowHiddenFiles = "preview:showhiddenfiles"
|
||||
|
||||
ConfigKey_TabPreset = "tab:preset"
|
||||
|
@ -33,17 +33,23 @@ const AnySchema = `
|
||||
`
|
||||
|
||||
type SettingsType struct {
|
||||
AiClear bool `json:"ai:*,omitempty"`
|
||||
AiPreset string `json:"ai:preset,omitempty"`
|
||||
AiApiType string `json:"ai:apitype,omitempty"`
|
||||
AiBaseURL string `json:"ai:baseurl,omitempty"`
|
||||
AiApiToken string `json:"ai:apitoken,omitempty"`
|
||||
AiName string `json:"ai:name,omitempty"`
|
||||
AiModel string `json:"ai:model,omitempty"`
|
||||
AiOrgID string `json:"ai:orgid,omitempty"`
|
||||
AIApiVersion string `json:"ai:apiversion,omitempty"`
|
||||
AiMaxTokens float64 `json:"ai:maxtokens,omitempty"`
|
||||
AiTimeoutMs float64 `json:"ai:timeoutms,omitempty"`
|
||||
AppClear bool `json:"app:*,omitempty"`
|
||||
AppGlobalHotkey string `json:"app:globalhotkey,omitempty"`
|
||||
AppDismissArchitectureWarning bool `json:"app:dismissarchitecturewarning,omitempty"`
|
||||
|
||||
AiClear bool `json:"ai:*,omitempty"`
|
||||
AiPreset string `json:"ai:preset,omitempty"`
|
||||
AiApiType string `json:"ai:apitype,omitempty"`
|
||||
AiBaseURL string `json:"ai:baseurl,omitempty"`
|
||||
AiApiToken string `json:"ai:apitoken,omitempty"`
|
||||
AiName string `json:"ai:name,omitempty"`
|
||||
AiModel string `json:"ai:model,omitempty"`
|
||||
AiOrgID string `json:"ai:orgid,omitempty"`
|
||||
AIApiVersion string `json:"ai:apiversion,omitempty"`
|
||||
AiMaxTokens float64 `json:"ai:maxtokens,omitempty"`
|
||||
AiTimeoutMs float64 `json:"ai:timeoutms,omitempty"`
|
||||
AiFontSize float64 `json:"ai:fontsize,omitempty"`
|
||||
AiFixedFontSize float64 `json:"ai:fixedfontsize,omitempty"`
|
||||
|
||||
TermClear bool `json:"term:*,omitempty"`
|
||||
TermFontSize float64 `json:"term:fontsize,omitempty"`
|
||||
@ -54,10 +60,12 @@ type SettingsType struct {
|
||||
TermLocalShellOpts []string `json:"term:localshellopts,omitempty"`
|
||||
TermScrollback *int64 `json:"term:scrollback,omitempty"`
|
||||
TermCopyOnSelect *bool `json:"term:copyonselect,omitempty"`
|
||||
TermTransparency *float64 `json:"term:transparency,omitempty"`
|
||||
|
||||
EditorMinimapEnabled bool `json:"editor:minimapenabled,omitempty"`
|
||||
EditorStickyScrollEnabled bool `json:"editor:stickyscrollenabled,omitempty"`
|
||||
EditorWordWrap bool `json:"editor:wordwrap,omitempty"`
|
||||
EditorMinimapEnabled bool `json:"editor:minimapenabled,omitempty"`
|
||||
EditorStickyScrollEnabled bool `json:"editor:stickyscrollenabled,omitempty"`
|
||||
EditorWordWrap bool `json:"editor:wordwrap,omitempty"`
|
||||
EditorFontSize float64 `json:"editor:fontsize,omitempty"`
|
||||
|
||||
WebClear bool `json:"web:*,omitempty"`
|
||||
WebOpenLinksInternally bool `json:"web:openlinksinternally,omitempty"`
|
||||
@ -73,6 +81,9 @@ type SettingsType struct {
|
||||
AutoUpdateInstallOnQuit bool `json:"autoupdate:installonquit,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"`
|
||||
|
||||
TabPreset string `json:"tab:preset,omitempty"`
|
||||
|
@ -28,7 +28,8 @@ func SwitchWorkspace(ctx context.Context, windowId string, workspaceId string) (
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting window: %w", err)
|
||||
}
|
||||
if window.WorkspaceId == workspaceId {
|
||||
curWsId := window.WorkspaceId
|
||||
if curWsId == workspaceId {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@ -45,24 +46,24 @@ func SwitchWorkspace(ctx context.Context, windowId string, workspaceId string) (
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
curWs, err := GetWorkspace(ctx, window.WorkspaceId)
|
||||
window.WorkspaceId = workspaceId
|
||||
err = wstore.DBUpdate(ctx, window)
|
||||
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 {
|
||||
return nil, fmt.Errorf("error deleting current workspace: %w", err)
|
||||
}
|
||||
if !deleted {
|
||||
log.Printf("current workspace %s was not deleted\n", curWs.OID)
|
||||
log.Printf("current workspace %s was not deleted\n", curWsId)
|
||||
} 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)
|
||||
return ws, wstore.DBUpdate(ctx, window)
|
||||
return ws, nil
|
||||
}
|
||||
|
||||
func GetWindow(ctx context.Context, windowId string) (*waveobj.Window, error) {
|
||||
|
@ -122,6 +122,7 @@ func DeleteWorkspace(ctx context.Context, workspaceId string, force bool) (bool,
|
||||
return false, fmt.Errorf("error closing tab: %w", err)
|
||||
}
|
||||
}
|
||||
windowId, err := wstore.DBFindWindowForWorkspaceId(ctx, workspaceId)
|
||||
err = wstore.DBDelete(ctx, waveobj.OType_Workspace, workspaceId)
|
||||
if err != nil {
|
||||
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)
|
||||
wps.Broker.Publish(wps.WaveEvent{
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -247,6 +247,12 @@ func NotifyCommand(w *wshutil.WshRpc, data wshrpc.WaveNotificationOptions, opts
|
||||
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
|
||||
func RemoteFileDeleteCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error {
|
||||
_, err := sendRpcRequestCallHelper[any](w, "remotefiledelete", data, opts)
|
||||
|
@ -149,6 +149,7 @@ type WshRpcInterface interface {
|
||||
ActivityCommand(ctx context.Context, data ActivityUpdate) error
|
||||
GetVarCommand(ctx context.Context, data CommandVarData) (*CommandVarResponseData, error)
|
||||
SetVarCommand(ctx context.Context, data CommandVarData) error
|
||||
PathCommand(ctx context.Context, data PathCommandData) (string, error)
|
||||
|
||||
// connection functions
|
||||
ConnStatusCommand(ctx context.Context) ([]ConnStatus, error)
|
||||
@ -605,6 +606,13 @@ type CommandVarResponseData struct {
|
||||
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 {
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
|
@ -12,10 +12,12 @@ import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
"github.com/wavetermdev/waveterm/pkg/blockcontroller"
|
||||
"github.com/wavetermdev/waveterm/pkg/filestore"
|
||||
"github.com/wavetermdev/waveterm/pkg/panichandler"
|
||||
@ -183,6 +185,7 @@ func (ws *WshServer) CreateBlockCommand(ctx context.Context, data wshrpc.Command
|
||||
ActionType: wcore.LayoutActionDataType_Insert,
|
||||
BlockId: blockData.OID,
|
||||
Magnified: data.Magnified,
|
||||
Ephemeral: data.Ephemeral,
|
||||
Focused: true,
|
||||
})
|
||||
if err != nil {
|
||||
@ -829,3 +832,39 @@ func (ws *WshServer) SetVarCommand(ctx context.Context, data wshrpc.CommandVarDa
|
||||
envStr := envutil.MapToEnv(envMap)
|
||||
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
|
||||
}
|
||||
|
@ -342,7 +342,10 @@ func (w *WshRpc) runServer() {
|
||||
continue
|
||||
}
|
||||
if msg.IsRpcRequest() {
|
||||
go w.handleRequest(&msg)
|
||||
go func() {
|
||||
defer panichandler.PanicHandler("handleRequest:goroutine")
|
||||
w.handleRequest(&msg)
|
||||
}()
|
||||
} else {
|
||||
respCh := w.getResponseCh(msg.ResId)
|
||||
if respCh == nil {
|
||||
|
207
yarn.lock
207
yarn.lock
@ -6450,12 +6450,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^22.10.1":
|
||||
version: 22.10.1
|
||||
resolution: "@types/node@npm:22.10.1"
|
||||
"@types/node@npm:^22.10.2":
|
||||
version: 22.10.2
|
||||
resolution: "@types/node@npm:22.10.2"
|
||||
dependencies:
|
||||
undici-types: "npm:~6.20.0"
|
||||
checksum: 10c0/0fbb6d29fa35d807f0223a4db709c598ac08d66820240a2cd6a8a69b8f0bc921d65b339d850a666b43b4e779f967e6ed6cf6f0fca3575e08241e6b900364c234
|
||||
checksum: 10c0/2c7b71a040f1ef5320938eca8ebc946e6905caa9bbf3d5665d9b3774a8d15ea9fab1582b849a6d28c7fc80756a62c5666bc66b69f42f4d5dafd1ccb193cdb4ac
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -6502,9 +6502,9 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@types/prop-types@npm:*, @types/prop-types@npm:^15":
|
||||
version: 15.7.13
|
||||
resolution: "@types/prop-types@npm:15.7.13"
|
||||
checksum: 10c0/1b20fc67281902c6743379960247bc161f3f0406ffc0df8e7058745a85ea1538612109db0406290512947f9632fe9e10e7337bf0ce6338a91d6c948df16a7c61
|
||||
version: 15.7.14
|
||||
resolution: "@types/prop-types@npm:15.7.14"
|
||||
checksum: 10c0/1ec775160bfab90b67a782d735952158c7e702ca4502968aa82565bd8e452c2de8601c8dfe349733073c31179116cf7340710160d3836aa8a1ef76d1532893b1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -6793,15 +6793,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/eslint-plugin@npm:8.17.0":
|
||||
version: 8.17.0
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.17.0"
|
||||
"@typescript-eslint/eslint-plugin@npm:8.18.0":
|
||||
version: 8.18.0
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.18.0"
|
||||
dependencies:
|
||||
"@eslint-community/regexpp": "npm:^4.10.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.17.0"
|
||||
"@typescript-eslint/type-utils": "npm:8.17.0"
|
||||
"@typescript-eslint/utils": "npm:8.17.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.17.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.18.0"
|
||||
"@typescript-eslint/type-utils": "npm:8.18.0"
|
||||
"@typescript-eslint/utils": "npm:8.18.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.18.0"
|
||||
graphemer: "npm:^1.4.0"
|
||||
ignore: "npm:^5.3.1"
|
||||
natural-compare: "npm:^1.4.0"
|
||||
@ -6809,108 +6809,99 @@ __metadata:
|
||||
peerDependencies:
|
||||
"@typescript-eslint/parser": ^8.0.0 || ^8.0.0-alpha.0
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
checksum: 10c0/d78778173571a9a1370345bc2aa3e850235a489d16b8a8b5ba3086b988bbef7549bdae38e509d7a679ba3179c688cc5a408376b158be402770836e94ffc9602d
|
||||
typescript: ">=4.8.4 <5.8.0"
|
||||
checksum: 10c0/c338da1b96c41d7b94401a6711659d0fef3acb691eff7a958f9d3aa0442a858830daad67e3575288a4f4669572e2b690517a513519b404a465ad68fe0a82d3ec
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/parser@npm:8.17.0":
|
||||
version: 8.17.0
|
||||
resolution: "@typescript-eslint/parser@npm:8.17.0"
|
||||
"@typescript-eslint/parser@npm:8.18.0":
|
||||
version: 8.18.0
|
||||
resolution: "@typescript-eslint/parser@npm:8.18.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager": "npm:8.17.0"
|
||||
"@typescript-eslint/types": "npm:8.17.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.17.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.17.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.18.0"
|
||||
"@typescript-eslint/types": "npm:8.18.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.18.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.18.0"
|
||||
debug: "npm:^4.3.4"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
checksum: 10c0/2543deadf01302a92d3b6f58a4c14f98d8936c4d976e7da05e3bb65608f19d8de93b25282e343c304eca3e3f37f2ac23e97fa9c11c6edff36dd2d4f6b601a630
|
||||
typescript: ">=4.8.4 <5.8.0"
|
||||
checksum: 10c0/d3a062511c24dfcf522a645db1153022d49aa3bb05e288c22474cf04dc1d836f877eb9d2733947e448981ffb16e4de50d4ebe7570a268733a641f228ca6c4849
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/scope-manager@npm:8.17.0":
|
||||
version: 8.17.0
|
||||
resolution: "@typescript-eslint/scope-manager@npm:8.17.0"
|
||||
"@typescript-eslint/scope-manager@npm:8.18.0":
|
||||
version: 8.18.0
|
||||
resolution: "@typescript-eslint/scope-manager@npm:8.18.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.17.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.17.0"
|
||||
checksum: 10c0/0c08d14240bad4b3f6874f08ba80b29db1a6657437089a6f109db458c544d835bcdc06ba9140bb4f835233ba4326d9a86e6cf6bdb5209960d2f7025aa3191f4f
|
||||
"@typescript-eslint/types": "npm:8.18.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.18.0"
|
||||
checksum: 10c0/6bf6532fd43f2b55b9b47fa8b0217c5b5a03f022e869a6a21228fc3ae04c0ac6c5ae5d6026866d189ba424d2f98cc6fbd2a34f909d241c9b86c031afd808f90c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/type-utils@npm:8.17.0":
|
||||
version: 8.17.0
|
||||
resolution: "@typescript-eslint/type-utils@npm:8.17.0"
|
||||
"@typescript-eslint/type-utils@npm:8.18.0":
|
||||
version: 8.18.0
|
||||
resolution: "@typescript-eslint/type-utils@npm:8.18.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree": "npm:8.17.0"
|
||||
"@typescript-eslint/utils": "npm:8.17.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.18.0"
|
||||
"@typescript-eslint/utils": "npm:8.18.0"
|
||||
debug: "npm:^4.3.4"
|
||||
ts-api-utils: "npm:^1.3.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
checksum: 10c0/6138ec71b5692d4b5e0bf3d7f66a6fa4e91ddea7031907b0ac45a7693df0a2f4cc5bca7218311e0639620d636ceb7efec83a137dfcd5938304d873b774fcc8bd
|
||||
typescript: ">=4.8.4 <5.8.0"
|
||||
checksum: 10c0/c0fcf201c3b53f9374c0571198a639c81536170141caa08fd0f47094a596b1f82f839a849eac5832f954345c567dccb45b2ee1c0872c513331165f7bcb812396
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/types@npm:8.17.0":
|
||||
version: 8.17.0
|
||||
resolution: "@typescript-eslint/types@npm:8.17.0"
|
||||
checksum: 10c0/26b1bf9dfc3ee783c85c6f354b84c28706d5689d777f3ff2de2cb496e45f9d0189c0d561c03ccbc8b24712438be17cf63dd0871ff3ca2083e7f48749770d1893
|
||||
"@typescript-eslint/types@npm:8.18.0":
|
||||
version: 8.18.0
|
||||
resolution: "@typescript-eslint/types@npm:8.18.0"
|
||||
checksum: 10c0/2dd7468c3f1c305545268b72c3a333488e6ab1b628c5f65081d895866422b9376c21634a7aac437805f84b22e352b6a8fc4dcf925ef4a8fd7d1898b8359f71be
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/typescript-estree@npm:8.17.0":
|
||||
version: 8.17.0
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:8.17.0"
|
||||
"@typescript-eslint/typescript-estree@npm:8.18.0":
|
||||
version: 8.18.0
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:8.18.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.17.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.17.0"
|
||||
"@typescript-eslint/types": "npm:8.18.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.18.0"
|
||||
debug: "npm:^4.3.4"
|
||||
fast-glob: "npm:^3.3.2"
|
||||
is-glob: "npm:^4.0.3"
|
||||
minimatch: "npm:^9.0.4"
|
||||
semver: "npm:^7.6.0"
|
||||
ts-api-utils: "npm:^1.3.0"
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
checksum: 10c0/523013f9b5cf2c58c566868e4c3b0b9ac1b4807223a6d64e2a7c58e01e53b6587ba61f1a8241eade361f3f426d6057657515473176141ef8aebb352bc0d223ce
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <5.8.0"
|
||||
checksum: 10c0/87b432b190b627314f007b17b2371898db78baaa3df67a0d9a94d080d88a7a307906b54a735084cacef37f6421e2b9c3320040617e73fe54eac2bf22c610f1ec
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/utils@npm:8.17.0":
|
||||
version: 8.17.0
|
||||
resolution: "@typescript-eslint/utils@npm:8.17.0"
|
||||
"@typescript-eslint/utils@npm:8.18.0":
|
||||
version: 8.18.0
|
||||
resolution: "@typescript-eslint/utils@npm:8.18.0"
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils": "npm:^4.4.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.17.0"
|
||||
"@typescript-eslint/types": "npm:8.17.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.17.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.18.0"
|
||||
"@typescript-eslint/types": "npm:8.18.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.18.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
checksum: 10c0/a9785ae5f7e7b51d521dc3f48b15093948e4fcd03352c0b60f39bae366cbc935947d215f91e2ae3182d52fa6affb5ccbb50feff487bd1209011f3e0da02cdf07
|
||||
typescript: ">=4.8.4 <5.8.0"
|
||||
checksum: 10c0/58a2fc1e404d1f905c2a958d995824eb4abc6e73836b186717550677f8b1d17954acc369feddb83277350915388bc3d8b721423c37777b8b8017fc29c89ec6ee
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/visitor-keys@npm:8.17.0":
|
||||
version: 8.17.0
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:8.17.0"
|
||||
"@typescript-eslint/visitor-keys@npm:8.18.0":
|
||||
version: 8.18.0
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:8.18.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.17.0"
|
||||
"@typescript-eslint/types": "npm:8.18.0"
|
||||
eslint-visitor-keys: "npm:^4.2.0"
|
||||
checksum: 10c0/9144c4e4a63034fb2031a0ee1fc77e80594f30cab3faafa9a1f7f83782695774dd32fac8986f260698b4e150b4dd52444f2611c07e4c101501f08353eb47c82c
|
||||
checksum: 10c0/d4cdc2adab553098b5be7117fb7df76fb66cfd380528881a0a8c2a9eee03bf8baddda07d15ca0bd3ed8b35c379b3f449292183df18e3e81898dbcadafcb708b8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -10507,16 +10498,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"electron@npm:^33.2.0":
|
||||
version: 33.2.0
|
||||
resolution: "electron@npm:33.2.0"
|
||||
"electron@npm:^33.2.1":
|
||||
version: 33.3.0
|
||||
resolution: "electron@npm:33.3.0"
|
||||
dependencies:
|
||||
"@electron/get": "npm:^2.0.0"
|
||||
"@types/node": "npm:^20.9.0"
|
||||
extract-zip: "npm:^2.0.1"
|
||||
bin:
|
||||
electron: cli.js
|
||||
checksum: 10c0/ad013bcc4fe65f6e0f40b77387452966347d4fb54acc8d8d7b0310244f73511a7ac87e03e1097970f83b94bcafc44e0b98a4c603daa9c95e11931d7c020ff155
|
||||
checksum: 10c0/ce381fb63da60b52b131aea473568aad2bff58cc3d8fd6fc68b4d0c0d5485e381e1a576727477ce901f4bd017baaa9e66f3f04da6ff698229343193258a8d38e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -16299,6 +16290,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 7.1.0
|
||||
resolution: "parse5-htmlparser2-tree-adapter@npm:7.1.0"
|
||||
@ -19358,9 +19356,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"sass@npm:^1.82.0":
|
||||
version: 1.82.0
|
||||
resolution: "sass@npm:1.82.0"
|
||||
"sass@npm:^1.83.0":
|
||||
version: 1.83.0
|
||||
resolution: "sass@npm:1.83.0"
|
||||
dependencies:
|
||||
"@parcel/watcher": "npm:^2.4.1"
|
||||
chokidar: "npm:^4.0.0"
|
||||
@ -19371,7 +19369,7 @@ __metadata:
|
||||
optional: true
|
||||
bin:
|
||||
sass: sass.js
|
||||
checksum: 10c0/7f86fe6ade4f6018862c448ed69d5c52f485b0125c9dab24e63f679739a04cc7c56562d588e3cf16b5efb4d2c4d0530e62740e1cfd273e2e3707d04d11011736
|
||||
checksum: 10c0/4415361229879a9041d77c953da85482e89032aa4321ba13250a9987d39c80fac6c88af3777f2a2d76a4e8b0c8afbd21c1970fdbe84e0b3ec25fb26741f92beb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -20962,19 +20960,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript-eslint@npm:^8.17.0":
|
||||
version: 8.17.0
|
||||
resolution: "typescript-eslint@npm:8.17.0"
|
||||
"typescript-eslint@npm:^8.18.0":
|
||||
version: 8.18.0
|
||||
resolution: "typescript-eslint@npm:8.18.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin": "npm:8.17.0"
|
||||
"@typescript-eslint/parser": "npm:8.17.0"
|
||||
"@typescript-eslint/utils": "npm:8.17.0"
|
||||
"@typescript-eslint/eslint-plugin": "npm:8.18.0"
|
||||
"@typescript-eslint/parser": "npm:8.18.0"
|
||||
"@typescript-eslint/utils": "npm:8.18.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
checksum: 10c0/b148525769b9afa789ad3c2d52249fa78e67a225d48d17f2f0117b0e8b52566112be3a35de6cd26bcaffba3114be87c1070f7f4b4e2b730c059668fec4a530bc
|
||||
typescript: ">=4.8.4 <5.8.0"
|
||||
checksum: 10c0/dda882cbfc1ebad6903864571bc69bfd7e32e17fec67d98fdfab2bd652348d425c6a1c3697734d59cd5dd15d26d496db3c3808c1de5840fa29b9e76184fa1865
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -21675,9 +21671,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vite-tsconfig-paths@npm:^5.1.3":
|
||||
version: 5.1.3
|
||||
resolution: "vite-tsconfig-paths@npm:5.1.3"
|
||||
"vite-tsconfig-paths@npm:^5.1.4":
|
||||
version: 5.1.4
|
||||
resolution: "vite-tsconfig-paths@npm:5.1.4"
|
||||
dependencies:
|
||||
debug: "npm:^4.1.1"
|
||||
globrex: "npm:^0.1.2"
|
||||
@ -21687,7 +21683,7 @@ __metadata:
|
||||
peerDependenciesMeta:
|
||||
vite:
|
||||
optional: true
|
||||
checksum: 10c0/fb7480efa31fd50439f4a12c91bc953e5cc09d69fdc7eeb6ffff7cc796bc2c1f2c617c3abfdcbf5d7414848076cea9deb60bc002142f93b6e3131e5458760710
|
||||
checksum: 10c0/6228f23155ea25d92b1e1702284cf8dc52ad3c683c5ca691edd5a4c82d2913e7326d00708cef1cbfde9bb226261df0e0a12e03ef1d43b6a92d8f02b483ef37e3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -21734,9 +21730,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vite@npm:^6.0.2":
|
||||
version: 6.0.2
|
||||
resolution: "vite@npm:6.0.2"
|
||||
"vite@npm:^6.0.3":
|
||||
version: 6.0.3
|
||||
resolution: "vite@npm:6.0.3"
|
||||
dependencies:
|
||||
esbuild: "npm:^0.24.0"
|
||||
fsevents: "npm:~2.3.3"
|
||||
@ -21782,7 +21778,7 @@ __metadata:
|
||||
optional: true
|
||||
bin:
|
||||
vite: bin/vite.js
|
||||
checksum: 10c0/45fc609f2bc5fb5beb5a8e2cad9ad6c2edce229a922f6fc1270ea2a9d75819482edcc0f77c85b4a7abdad7eb69ce6a4f26131925d47cdc0778fc15d1bbc3b6a2
|
||||
checksum: 10c0/764ebed14770426a638575b23a51127c630ace873999ab896b0184484d8107e7255cdf64cfb36c65c1ef1d583e44b70a1d14c0f05b89612e834a5806e3964475
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -21937,7 +21933,7 @@ __metadata:
|
||||
remark-preset-lint-recommended: "npm:^7.0.0"
|
||||
remark-typescript-code-import: "npm:^1.0.1"
|
||||
typescript: "npm:^5.7.2"
|
||||
typescript-eslint: "npm:^8.17.0"
|
||||
typescript-eslint: "npm:^8.18.0"
|
||||
ua-parser-js: "npm:^2.0.0"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
@ -21970,7 +21966,7 @@ __metadata:
|
||||
"@types/css-tree": "npm:^2"
|
||||
"@types/debug": "npm:^4"
|
||||
"@types/electron": "npm:^1.6.12"
|
||||
"@types/node": "npm:^22.10.1"
|
||||
"@types/node": "npm:^22.10.2"
|
||||
"@types/papaparse": "npm:^5"
|
||||
"@types/pngjs": "npm:^6.0.5"
|
||||
"@types/prop-types": "npm:^15"
|
||||
@ -21997,7 +21993,7 @@ __metadata:
|
||||
css-tree: "npm:^3.0.1"
|
||||
dayjs: "npm:^1.11.13"
|
||||
debug: "npm:^4.3.7"
|
||||
electron: "npm:^33.2.0"
|
||||
electron: "npm:^33.2.1"
|
||||
electron-builder: "npm:^25.1.8"
|
||||
electron-updater: "npm:6.3.9"
|
||||
electron-vite: "npm:^2.3.0"
|
||||
@ -22014,6 +22010,7 @@ __metadata:
|
||||
overlayscrollbars: "npm:^2.10.1"
|
||||
overlayscrollbars-react: "npm:^0.5.6"
|
||||
papaparse: "npm:^5.4.1"
|
||||
parse-srcset: "npm:^1.0.2"
|
||||
pngjs: "npm:^7.0.0"
|
||||
prettier: "npm:^3.4.2"
|
||||
prettier-plugin-jsdoc: "npm:^1.3.0"
|
||||
@ -22035,7 +22032,7 @@ __metadata:
|
||||
remark-github-blockquote-alert: "npm:^1.3.0"
|
||||
rollup-plugin-flow: "npm:^1.1.1"
|
||||
rxjs: "npm:^7.8.1"
|
||||
sass: "npm:^1.82.0"
|
||||
sass: "npm:^1.83.0"
|
||||
semver: "npm:^7.6.3"
|
||||
sharp: "npm:^0.33.5"
|
||||
shell-quote: "npm:^1.8.2"
|
||||
@ -22048,13 +22045,13 @@ __metadata:
|
||||
tslib: "npm:^2.8.1"
|
||||
tsx: "npm:^4.19.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"
|
||||
vite: "npm:^6.0.2"
|
||||
vite: "npm:^6.0.3"
|
||||
vite-plugin-image-optimizer: "npm:^1.1.8"
|
||||
vite-plugin-static-copy: "npm:^2.2.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"
|
||||
winston: "npm:^3.17.0"
|
||||
ws: "npm:^8.18.0"
|
||||
|
Loading…
Reference in New Issue
Block a user