diff --git a/.github/workflows/build-helper.yml b/.github/workflows/build-helper.yml index d10c0d9ec..d0d2e1d0d 100644 --- a/.github/workflows/build-helper.yml +++ b/.github/workflows/build-helper.yml @@ -11,6 +11,7 @@ on: env: GO_VERSION: "1.22" NODE_VERSION: "20" + STATIC_DOCSITE_PATH: docsite jobs: runbuild: permissions: @@ -110,6 +111,15 @@ jobs: smctl windows certsync shell: cmd + - name: Download waveterm-docs static site + uses: dawidd6/action-download-artifact@v6 + with: + github_token: ${{secrets.GITHUB_TOKEN}} + workflow: build-embedded.yml + repo: wavetermdev/waveterm-docs + name: static-site + path: ${{env.STATIC_DOCSITE_PATH}} + # Build and upload packages - name: Build (not Windows) if: matrix.platform != 'windows' @@ -121,6 +131,7 @@ jobs: APPLE_ID: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_NOTARIZATION_APPLE_ID_2 }} APPLE_APP_SPECIFIC_PASSWORD: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_NOTARIZATION_PWD_2 }} APPLE_TEAM_ID: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_NOTARIZATION_TEAM_ID_2 }} + STATIC_DOCSITE_PATH: ${{env.STATIC_DOCSITE_PATH}} - name: Build (Windows only) if: matrix.platform == 'windows' run: task package @@ -128,6 +139,7 @@ jobs: USE_SYSTEM_FPM: true # Ensure that the installed version of FPM is used rather than the bundled one. CSC_LINK: ${{ steps.variables.outputs.SM_CLIENT_CERT_FILE }} CSC_KEY_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }} + STATIC_DOCSITE_PATH: ${{env.STATIC_DOCSITE_PATH}} shell: powershell # electron-builder's Windows code signing package has some compatibility issues with pwsh, so we need to use Windows Powershell - name: Upload to S3 staging run: task artifacts:upload diff --git a/electron-builder.config.cjs b/electron-builder.config.cjs index c5cfdc26b..89841c218 100644 --- a/electron-builder.config.cjs +++ b/electron-builder.config.cjs @@ -34,6 +34,7 @@ const config = { }, asarUnpack: [ "dist/bin/**/*", // wavesrv and wsh binaries + "dist/docsite/**/*", // the static docsite ], mac: { target: [ @@ -86,6 +87,14 @@ const config = { provider: "generic", url: "https://dl.waveterm.dev/releases-w2", }, + beforePack: () => { + const staticSourcePath = process.env.STATIC_DOCSITE_PATH; + const staticDestPath = "dist/docsite"; + if (staticSourcePath) { + console.log(`Static docsite path is specified, copying from "${staticSourcePath}" to "${staticDestPath}"`); + fs.cpSync(staticSourcePath, staticDestPath, { recursive: true }); + } + }, afterPack: (context) => { // This is a workaround to restore file permissions to the wavesrv binaries on macOS after packaging the universal binary. if (context.electronPlatformName === "darwin" && context.arch === Arch.universal) { diff --git a/emain/docsite.ts b/emain/docsite.ts new file mode 100644 index 000000000..ccdcc602b --- /dev/null +++ b/emain/docsite.ts @@ -0,0 +1,27 @@ +import { getWebServerEndpoint } from "@/util/endpoints"; +import { fetch } from "@/util/fetchutil"; +import { ipcMain } from "electron"; + +const docsiteWebUrl = "https://docs.waveterm.dev/"; +let docsiteUrl: string; + +ipcMain.on("get-docsite-url", (event) => { + event.returnValue = docsiteUrl; +}); + +export async function initDocsite() { + const docsiteEmbeddedUrl = getWebServerEndpoint() + "/docsite/"; + try { + const response = await fetch(docsiteEmbeddedUrl); + if (response.ok) { + console.log("Embedded docsite is running, using embedded version for help view"); + docsiteUrl = docsiteEmbeddedUrl; + } else { + console.log("Embedded docsite is not running, using web version for help view", response); + docsiteUrl = docsiteWebUrl; + } + } catch (error) { + console.log("Failed to fetch docsite url, using web version for help view", error); + docsiteUrl = docsiteWebUrl; + } +} diff --git a/emain/emain.ts b/emain/emain.ts index 29a7ee68f..dd7013cc7 100644 --- a/emain/emain.ts +++ b/emain/emain.ts @@ -20,12 +20,13 @@ import { fetch } from "../frontend/util/fetchutil"; import * as keyutil from "../frontend/util/keyutil"; import { fireAndForget } from "../frontend/util/util"; import { AuthKey, AuthKeyEnv, configureAuthKeyRequestInjection } from "./authkey"; +import { initDocsite } from "./docsite"; import { ElectronWshClient, initElectronWshClient } from "./emain-wsh"; import { getLaunchSettings } from "./launchsettings"; import { getAppMenu } from "./menu"; import { getElectronAppBasePath, - getGoAppBasePath, + getElectronAppUnpackedBasePath, getWaveHomeDir, getWaveSrvCwd, getWaveSrvPath, @@ -95,7 +96,7 @@ console.log( "waveterm-app starting, WAVETERM_HOME=%s, electronpath=%s gopath=%s arch=%s/%s", waveHome, getElectronAppBasePath(), - getGoAppBasePath(), + getElectronAppUnpackedBasePath(), unamePlatform, unameArch ) @@ -155,7 +156,7 @@ function runWaveSrv(): Promise { pReject = argReject; }); const envCopy = { ...process.env }; - envCopy[WaveAppPathVarName] = getGoAppBasePath(); + envCopy[WaveAppPathVarName] = getElectronAppUnpackedBasePath(); envCopy[WaveSrvReadySignalPidVarName] = process.pid.toString(); envCopy[AuthKeyEnv] = AuthKey; const waveSrvCmd = getWaveSrvPath(); @@ -871,6 +872,7 @@ async function appMain() { await electronApp.whenReady(); configureAuthKeyRequestInjection(electron.session.defaultSession); await relaunchBrowserWindows(); + await initDocsite(); setTimeout(runActiveTimer, 5000); // start active timer, wait 5s just to be safe try { initElectronWshClient(); diff --git a/emain/platform.ts b/emain/platform.ts index 8e67d12e4..22d5e8651 100644 --- a/emain/platform.ts +++ b/emain/platform.ts @@ -50,7 +50,7 @@ function getElectronAppBasePath(): string { return path.dirname(import.meta.dirname); } -function getGoAppBasePath(): string { +function getElectronAppUnpackedBasePath(): string { return getElectronAppBasePath().replace("app.asar", "app.asar.unpacked"); } @@ -59,10 +59,10 @@ const wavesrvBinName = `wavesrv.${unameArch}`; function getWaveSrvPath(): string { if (process.platform === "win32") { const winBinName = `${wavesrvBinName}.exe`; - const appPath = path.join(getGoAppBasePath(), "bin", winBinName); + const appPath = path.join(getElectronAppUnpackedBasePath(), "bin", winBinName); return `${appPath}`; } - return path.join(getGoAppBasePath(), "bin", wavesrvBinName); + return path.join(getElectronAppUnpackedBasePath(), "bin", wavesrvBinName); } function getWaveSrvCwd(): string { @@ -71,7 +71,7 @@ function getWaveSrvCwd(): string { export { getElectronAppBasePath, - getGoAppBasePath, + getElectronAppUnpackedBasePath, getWaveHomeDir, getWaveSrvCwd, getWaveSrvPath, diff --git a/emain/preload.ts b/emain/preload.ts index 96881a369..4ad0bb2c4 100644 --- a/emain/preload.ts +++ b/emain/preload.ts @@ -11,6 +11,7 @@ contextBridge.exposeInMainWorld("api", { getUserName: () => ipcRenderer.sendSync("get-user-name"), getHostName: () => ipcRenderer.sendSync("get-host-name"), getAboutModalDetails: () => ipcRenderer.sendSync("get-about-modal-details"), + getDocsiteUrl: () => ipcRenderer.sendSync("get-docsite-url"), openNewWindow: () => ipcRenderer.send("open-new-window"), showContextMenu: (menu, position) => ipcRenderer.send("contextmenu-show", menu, position), onContextMenuClick: (callback) => ipcRenderer.on("contextmenu-click", (_event, id) => callback(id)), diff --git a/frontend/app/view/helpview/helpview.less b/frontend/app/view/helpview/helpview.less index 0c5048f47..9e5c7263c 100644 --- a/frontend/app/view/helpview/helpview.less +++ b/frontend/app/view/helpview/helpview.less @@ -3,15 +3,10 @@ .help-view { width: 100%; - padding: 0 5px; - - .title { - font-weight: bold; - } - - code { - font: var(--fixed-font); - background-color: var(--highlight-bg-color); - padding: 0 5px; + height: 100%; + .docsite-webview { + width: 100%; + height: 100%; + border: 0; } } diff --git a/frontend/app/view/helpview/helpview.tsx b/frontend/app/view/helpview/helpview.tsx index 1fe722042..5d30d3e0f 100644 --- a/frontend/app/view/helpview/helpview.tsx +++ b/frontend/app/view/helpview/helpview.tsx @@ -1,208 +1,15 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { Markdown } from "@/app/element/markdown"; -import { globalStore } from "@/app/store/global"; -import { Atom, atom, PrimitiveAtom } from "jotai"; +import { getApi } from "@/app/store/global"; +import { useState } from "react"; import "./helpview.less"; -const helpText = ` -For more up-to-date documentation, please visit [our docs site](http://docs.waveterm.dev) - - -## Blocks -Every individual Component is contained in its own block. These can be added, removed, moved and resized. Each block has its own header which can be right clicked to reveal more operations you can do with that block. - -### How to Add a Block -Adding a block can be done using the widget bar on the right hand side of the window. This will add a block of the selected type to the current tab. - -### How to Close a Block -Blocks can be closed by clicking the ✕ button on the right side of the header. Alternatively, the currently focused block can be closed by pressing \`Cmd + w\`. - -### How to Navigate Blocks -At most, it is possible to have one block be focused. Depending on the type of block, this allows you to directly interact with the content in that block. A focused block is always outlined with a distinct border. A block may be focused by clicking on it. Alternatively, you can change the focused block by pressing Ctrl + Shift + ↑, Ctrl + Shift + ↓, Ctrl + Shift + ←, or Ctrl + Shift + →to navigate relative to the currently selected block. -1 -### How to Magnify Blocks -Magnifying a block will pop the block out in front of everything else. You can magnify using the header icon, or with \`Cmd + m\`. - -### How to Reorganize Blocks -By dragging and dropping their headers, blocks can be moved to different locations in the layout. This effectively allows you to reorganize your screen however you see fit. When dragging, you will see a preview of the block that is being dragged. When the block is over a valid drop point, the area where it would be moved to will turn green. Releasing the click will place the block there and reflow the other blocks around it. If you see a green box cover half of two different blocks, the drop will place the block between the two. If you see the green box cover half of one block at the edge of the screen, the block will be placed between that block and the edge of the screen. If you see the green box cover one block entirely, the two blocks will swap locations. - -### How to Resize Blocks -Hovering the mouse between two blocks changes your cursor to ↔ and reveals a green line dividing the blocks. By dragging and dropping this green line, you are able to resize the blocks adjacent to it. - -## Types of Blocks - -### Term -The usual terminal you know and love. We add a few plugins via the \`wsh\` command that you can read more about further below. - -### Preview -Preview is the generic type of block used for viewing files. This can take many different forms based on the type of file being viewed. -You can use \`wsh view [path]\` from any Wave terminal window to open a preview block with the contents of the specified path (e.g. \`wsh view .\` or \`wsh view ~/myimage.jpg\`). - -#### Directory -When looking at a directory, preview will show a file viewer much like MacOS' *Finder* application or Windows' *File Explorer* application. This variant is slightly more geared toward software development with the focus on seeing what is shown by the \`ls -alh\` command. - -##### View a New File -The simplest way to view a new file is to double click its row in the file viewer. Alternatively, while the block is focused, you can use the ↑ and ↓ arrow keys to select a row and press enter to preview the associated file. - -##### View the Parent Directory -In the directory view, this is as simple as opening the \`..\` file as if it were a regular file. This can be done with the method above. You can also use the keyboard shortcut \`Cmd + ArrowUp\`. - -##### Navigate Back and Forward -When looking at a file, you can navigate back by clicking the back button in the block header or the keyboard shortcut \`Cmd + ArrowLeft\`. You can always navigate back and forward using \`Cmd + ArrowLeft\` and \`Cmd + ArrowRight\`. - -##### Filter the List of Files -While the block is focused, you can filter by filename by typing a substring of the filename you're working for. To clear the filter, you can click the ✕ on the filter dropdown or press esc. - -##### Sort by a File Column -To sort a file by a specific column, click on the header for that column. If you click the header again, it will reverse the sort order. - -##### Hide and Show Hidden Files -At the right of the block header, there is an 👁️ button. Clicking this button hides and shows hidden files. - -##### Refresh the Directory -At the right of the block header, there is a refresh button. Clicking this button refreshes the directory contents. - -##### Navigate to Common Directories -At the left of the block header, there is a file icon. Clicking and holding on this icon opens a menu where you can select a common folder to navigate to. The available options are *Home*, *Desktop*, *Downloads*, and *Root*. - -##### Open a New Terminal in the Current Directory -If you right click the header of the block (alternatively, click the gear icon), one of the menu items listed is **Open Terminal in New Block**. This will create a new terminal block at your current directory. - -##### Open a New Terminal in a Child Directory -If you want to open a terminal for a child directory instead, you can right click on that file's row to get the **Open Terminal in New Block** option. Clicking this will open a terminal at that directory. Note that this option is only available for children that are directories. - -##### Open a New Preview for a Child -To open a new Preview Block for a Child, you can right click on that file's row and select the **Open Preview in New Block** option. - -#### Markdown -Opening a markdown file will bring up a view of the rendered markdown. These files cannot be edited in the preview at this time. - -#### Images/Media -Opening a picture will bring up the image of that picture. Opening a video will bring up a player that lets you watch the video. - -### Codeedit -Opening most text files will open Codeedit to either view or edit the file. It is technically part of the Preview block, but it is important enough to be singled out. -After opening a codeedit block, it is often useful to magnify it (\`Cmd + m\`) to get a larger view. You can then -use the hotkeys below to switch to edit mode, make your edits, save, and then use \`Cmd + w\` to close the block (all without using the mouse!). - -#### Switch to Edit Mode -To switch to edit mode, click the edit button to the right of the header. This lets you edit the file contents with a regular monaco editor. -You can also switch to edit mode by pressing \`Cmd + e\`. - -#### Save an Edit -Once an edit has been made in **edit mode**, click the save button to the right of the header to save the contents. -You can also save by pressing \`Cmd + s\`. - -#### Exit Edit Mode Without Saving -To exit **edit mode** without saving, click the cancel button to the right of the header. -You can also exit without saving by pressing \`Cmd + r\`. - -### AI - -#### How to Ask an LLM a Question -Asking a question is as simple as typing a message in the prompt and pressing enter. By default, we forward messages to the *gpt-4o-mini* model through our server. - -#### How To Change The Model -See *settings help* for more info on how to configure your model. - -### Web -The Web block is basically a simple web browser. The forward and backwards navigation have been added to the header. -You can use \`wsh\` to interact with the web block's URL (see the wsh section below). - -### Cpu % -A small plot displaying the % of CPU in use over time. This is an example of a block that is capable of plotting streamed data. We plan to make this more generic in the future. - -## Tabs -Tabs are ways to organize your blocks into separate screens. They mostly work the way you're used to in other apps. - -### Create a New Tab -A tab can be created by clicking the plus button to the right of your currently existing tabs - -### Delete a Tab -Hovering a tab reveals an ✕ button to the right side of it. Clicking it removes the tab. Note that this will also remove the instances of the blocks it contains. - -### Change a Tab Name -Double clicking the current tab name makes it possible to change the name of your tab. You are limited to 10 glyphs in your tab name. Note that we say glyphs because it is possible to use multiple-character glyphs including emojis in your tab name. - -### Reorganize Tabs -Tabs can be reorganized by dragging and dropping them to the left and right of other tabs. - -## Theming -It is possible to style each tab individually. This is most-easily done by right clicking on your tab and going to the background menu. From there, you can select from five different pre-made styles. - -It is possible to get more fine-grained control of the styles as well. See *settings help* for more info. - -## wsh command - -The wsh command is always available from wave terminal windows. It is a powerful tool for interacting with Wave blocks and can bridge data between your CLI and the widget GUIs. - -### view -You can open a preview block with the contents of any file or directory by running: - -\`\`\` -wsh view [path] -\`\`\` - -You can use this command to easily preview images, markdown files, and directories. For code/text files this will open -a codeedit block which you can use to quickly edit the file using Wave's embedded graphical editor. - -### edit - -\`\`\` -wsh editor [path] -\`\`\` - -This will open up codeedit for the specified file. This is useful for quickly editing files on a local or remote machine in our graphical editor. This command will wait until the file is closed before exiting (unlike \`view\`) so you can set your \`$EDITOR\` to \`wsh edit\` for a seamless experience. You can combine this with a \`-m\` flag to open the editor in magnified mode. - -### getmeta - -You can view the metadata of any block by running: - -\`\`\` -wsh getmeta [blockid] -\`\`\` - -This is especially useful for preview and web blocks as you can see the file or url that they are pointing to and use that in your CLI scripts. - -### setmeta - -You can update any metadata key value pair for blocks (and tabs) by using the setmeta command: - -\`\`\` -wsh setmeta [blockid] [key]=[value] -wsh setmeta [blockid] file=~/myfile.txt -wsh setmeta [blockid] url=https://waveterm.dev/ -\`\`\` - -You can get block and tab ids by right clicking on the appropriate block and selecting "Copy BlockId". When you -update the metadata for a preview or web block you'll see the changes reflected instantly in the block. - -Other useful metadata values to override block titles, icons, colors, themes, etc. can be found in the documentation. - -`; - class HelpViewModel implements ViewModel { viewType: string; - showTocAtom: PrimitiveAtom; - endIconButtons: Atom; constructor() { this.viewType = "help"; - this.showTocAtom = atom(false); - this.endIconButtons = atom([ - { - elemtype: "iconbutton", - icon: "book", - title: "Table of Contents", - click: () => this.showTocToggle(), - }, - ] as IconButtonDecl[]); - } - - showTocToggle() { - globalStore.set(this.showTocAtom, !globalStore.get(this.showTocAtom)); } } @@ -210,8 +17,14 @@ function makeHelpViewModel() { return new HelpViewModel(); } -function HelpView({ model }: { model: HelpViewModel }) { - return ; +function HelpView({}: { model: HelpViewModel }) { + const [url] = useState(() => getApi().getDocsiteUrl()); + console.log(url); + return ( +
+ +
+ ); } export { HelpView, HelpViewModel, makeHelpViewModel }; diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index eccc5caa7..14ef43570 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -58,6 +58,7 @@ declare global { getUserName: () => string; getHostName: () => string; getAboutModalDetails: () => AboutModalDetails; + getDocsiteUrl: () => string; showContextMenu: (menu?: ElectronContextMenuItem[]) => void; onContextMenuClick: (callback: (id: string) => void) => void; onNavigate: (callback: (url: string) => void) => void; diff --git a/pkg/docsite/docsite.go b/pkg/docsite/docsite.go new file mode 100644 index 000000000..f6b44676d --- /dev/null +++ b/pkg/docsite/docsite.go @@ -0,0 +1,29 @@ +package docsite + +import ( + "log" + "net/http" + "os" + "path/filepath" + + "github.com/wavetermdev/waveterm/pkg/wavebase" +) + +var docsiteStaticPath = filepath.Join(wavebase.GetWaveAppPath(), "docsite") + +var docsiteHandler http.Handler + +func GetDocsiteHandler() http.Handler { + stat, err := os.Stat(docsiteStaticPath) + if docsiteHandler == nil { + log.Println("Docsite is nil, initializing") + if err == nil && stat.IsDir() { + log.Printf("Found static site at %s, serving\n", docsiteStaticPath) + docsiteHandler = http.FileServer(http.Dir(docsiteStaticPath)) + } else { + log.Println("Did not find static site, serving not found handler") + docsiteHandler = http.NotFoundHandler() + } + } + return docsiteHandler +} diff --git a/pkg/util/shellutil/shellutil.go b/pkg/util/shellutil/shellutil.go index 691684f99..1e6e6a3ab 100644 --- a/pkg/util/shellutil/shellutil.go +++ b/pkg/util/shellutil/shellutil.go @@ -32,9 +32,6 @@ var userShellRegexp = regexp.MustCompile(`^UserShell: (.*)$`) const DefaultShellPath = "/bin/bash" -const WaveAppPathVarName = "WAVETERM_APP_PATH" -const AppPathBinDir = "bin" - const ( ZshIntegrationDir = "shell/zsh" BashIntegrationDir = "shell/bash" @@ -231,7 +228,7 @@ func GetWshBaseName(version string, goos string, goarch string) string { } func GetWshBinaryPath(version string, goos string, goarch string) string { - return filepath.Join(os.Getenv(WaveAppPathVarName), AppPathBinDir, GetWshBaseName(version, goos, goarch)) + return filepath.Join(wavebase.GetWaveAppBinPath(), GetWshBaseName(version, goos, goarch)) } func InitRcFiles(waveHome string, wshBinDir string) error { @@ -319,7 +316,7 @@ func initCustomShellStartupFilesInternal() error { if err != nil { return fmt.Errorf("error copying wsh binary to bin: %v", err) } - log.Printf("wsh binary successfully %q copied to %q\n", wshBaseName, wshDstPath) + log.Printf("wsh binary successfully copied from %q to %q\n", wshBaseName, wshDstPath) return nil } diff --git a/pkg/wavebase/wavebase.go b/pkg/wavebase/wavebase.go index 77f0f74d8..a4c366919 100644 --- a/pkg/wavebase/wavebase.go +++ b/pkg/wavebase/wavebase.go @@ -35,6 +35,9 @@ const WaveDBDir = "db" const JwtSecret = "waveterm" // TODO generate and store this const ConfigDir = "config" +const WaveAppPathVarName = "WAVETERM_APP_PATH" +const AppPathBinDir = "bin" + var baseLock = &sync.Mutex{} var ensureDirCache = map[string]bool{} @@ -43,6 +46,14 @@ func IsDevMode() bool { return pdev != "" } +func GetWaveAppPath() string { + return os.Getenv(WaveAppPathVarName) +} + +func GetWaveAppBinPath() string { + return filepath.Join(GetWaveAppPath(), AppPathBinDir) +} + func GetHomeDir() string { homeVar, err := os.UserHomeDir() if err != nil { diff --git a/pkg/web/web.go b/pkg/web/web.go index 9af8cadd4..0e470490b 100644 --- a/pkg/web/web.go +++ b/pkg/web/web.go @@ -23,6 +23,7 @@ import ( "github.com/gorilla/handlers" "github.com/gorilla/mux" "github.com/wavetermdev/waveterm/pkg/authkey" + "github.com/wavetermdev/waveterm/pkg/docsite" "github.com/wavetermdev/waveterm/pkg/filestore" "github.com/wavetermdev/waveterm/pkg/service" "github.com/wavetermdev/waveterm/pkg/telemetry" @@ -441,6 +442,8 @@ func MakeUnixListener() (net.Listener, error) { return rtn, nil } +const docsitePrefix = "/docsite/" + // blocking func RunWebServer(listener net.Listener) { gr := mux.NewRouter() @@ -448,6 +451,7 @@ func RunWebServer(listener net.Listener) { gr.HandleFunc("/wave/file", WebFnWrap(WebFnOpts{AllowCaching: false}, handleWaveFile)) gr.HandleFunc("/wave/service", WebFnWrap(WebFnOpts{JsonErrors: true}, handleService)) gr.HandleFunc("/wave/log-active-state", WebFnWrap(WebFnOpts{JsonErrors: true}, handleLogActiveState)) + gr.PathPrefix(docsitePrefix).Handler(http.StripPrefix(docsitePrefix, docsite.GetDocsiteHandler())) handler := http.TimeoutHandler(gr, HttpTimeoutDuration, "Timeout") if wavebase.IsDevMode() { handler = handlers.CORS(handlers.AllowedOrigins([]string{"*"}))(handler)