diff --git a/src/browser/pages/global.css b/src/browser/pages/global.css index 96ab201f6..076808f3a 100644 --- a/src/browser/pages/global.css +++ b/src/browser/pages/global.css @@ -10,8 +10,17 @@ body { background: #272727; color: #f4f4f4; margin: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", - "Segoe UI Emoji", "Segoe UI Symbol"; + font-family: + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + Roboto, + Helvetica, + Arial, + sans-serif, + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol"; overflow: hidden; } diff --git a/src/common/api.ts b/src/common/api.ts index b8d934c6f..6f6784f98 100644 --- a/src/common/api.ts +++ b/src/common/api.ts @@ -33,8 +33,11 @@ export interface SessionResponse { } export interface RecentResponse { - readonly recent: ReadonlyArray - readonly running: ReadonlyArray + readonly paths: string[] +} + +export interface RunningResponse { + readonly applications: ReadonlyArray } export interface HealthRequest { diff --git a/src/common/http.ts b/src/common/http.ts index ae53364f5..7d286aa0d 100644 --- a/src/common/http.ts +++ b/src/common/http.ts @@ -19,6 +19,7 @@ export enum ApiEndpoint { applications = "/applications", recent = "/recent", run = "/run", + running = "/running", session = "/session", status = "/status", } diff --git a/src/node/app/api.ts b/src/node/app/api.ts index b0989bb7f..12c32f4af 100644 --- a/src/node/app/api.ts +++ b/src/node/app/api.ts @@ -1,13 +1,17 @@ import { field, logger } from "@coder/logger" import * as cp from "child_process" +import * as fs from "fs-extra" import * as http from "http" import * as net from "net" +import * as path from "path" +import * as url from "url" import * as WebSocket from "ws" import { Application, ApplicationsResponse, ClientMessage, RecentResponse, + RunningResponse, ServerMessage, SessionError, SessionResponse, @@ -22,6 +26,12 @@ interface ServerSession { readonly app: Application } +interface VsRecents { + [key: string]: (string | object)[] +} + +type VsSettings = [string, string][] + /** * API HTTP provider. */ @@ -29,7 +39,11 @@ export class ApiHttpProvider extends HttpProvider { private readonly ws = new WebSocket.Server({ noServer: true }) private readonly sessions = new Map() - public constructor(options: HttpProviderOptions, private readonly server: HttpServer) { + public constructor( + options: HttpProviderOptions, + private readonly server: HttpServer, + private readonly dataDir?: string, + ) { super(options) } @@ -60,6 +74,11 @@ export class ApiHttpProvider extends HttpProvider { return { content: await this.recent(), } as HttpResponse + case ApiEndpoint.running: + this.ensureMethod(request) + return { + content: await this.running(), + } as HttpResponse } return undefined } @@ -280,12 +299,58 @@ export class ApiHttpProvider extends HttpProvider { } /** - * Return recent sessions. + * Return VS Code's recent paths. */ public async recent(): Promise { + try { + if (!this.dataDir) { + throw new Error("data directory is not set") + } + + const state: VsSettings = JSON.parse(await fs.readFile(path.join(this.dataDir, "User/state/global.json"), "utf8")) + const setting = Array.isArray(state) && state.find((item) => item[0] === "recently.opened") + if (!setting) { + throw new Error("settings appear malformed") + } + + const paths: { [key: string]: Promise } = {} + Object.values(JSON.parse(setting[1]) as VsRecents).forEach((recents) => { + recents + .filter((recent) => typeof recent === "string") + .forEach((recent) => { + try { + const pathname = url.parse(recent as string).pathname + if (pathname && !paths[pathname]) { + paths[pathname] = new Promise((resolve) => { + fs.stat(pathname) + .then(() => resolve(pathname)) + .catch(() => resolve()) + }) + } + } catch (error) { + logger.debug("invalid path", field("path", recent)) + } + }) + }) + + return { + paths: await Promise.all(Object.values(paths)), + } + } catch (error) { + if (error.code !== "ENOENT") { + throw error + } + } + + return { paths: [] } + } + + /** + * Return running sessions. + */ + public async running(): Promise { return { - recent: [], // TODO - running: Array.from(this.sessions).map(([sessionId, session]) => ({ + applications: Array.from(this.sessions).map(([sessionId, session]) => ({ ...session.app, sessionId, })), diff --git a/src/node/app/app.ts b/src/node/app/app.ts index 298ba9744..2b48d840d 100644 --- a/src/node/app/app.ts +++ b/src/node/app/app.ts @@ -90,14 +90,14 @@ export class MainHttpProvider extends HttpProvider { } public async getRoot(route: Route): Promise { - const recent = await this.api.recent() + const running = await this.api.running() const apps = await this.api.installedApplications() const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/home.html") response.content = response.content .replace(/{{COMMIT}}/g, this.options.commit) .replace(/{{BASE}}/g, this.base(route)) .replace(/{{UPDATE:NAME}}/, await this.getUpdate()) - .replace(/{{APP_LIST:RUNNING}}/, this.getAppRows(recent.running)) + .replace(/{{APP_LIST:RUNNING}}/, this.getAppRows(running.applications)) .replace( /{{APP_LIST:EDITORS}}/, this.getAppRows(apps.filter((app) => app.categories && app.categories.includes("Editor"))), diff --git a/src/node/entry.ts b/src/node/entry.ts index 3d8df7d14..f64606de3 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -44,7 +44,7 @@ const main = async (args: Args): Promise => { } const httpServer = new HttpServer(options) - const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer) + const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, args["user-data-dir"]) const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, !args["disable-updates"]) httpServer.registerHttpProvider("/vscode", VscodeHttpProvider, args) httpServer.registerHttpProvider("/login", LoginHttpProvider)