feat: add app.getRecentDocuments() (#47924)

feat: add app.getRecentDocuments()

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
This commit is contained in:
trop[bot] 2025-08-06 19:35:04 +02:00 committed by GitHub
commit 33f4808182
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 142 additions and 11 deletions

View file

@ -776,6 +776,22 @@ bar, and on macOS, you can visit it from dock menu.
Clears the recent documents list. Clears the recent documents list.
### `app.getRecentDocuments()` _macOS_ _Windows_
Returns `string[]` - An array containing documents in the most recent documents list.
```js
const { app } = require('electron')
const path = require('node:path')
const file = path.join(app.getPath('desktop'), 'foo.txt')
app.addRecentDocument(file)
const recents = app.getRecentDocuments()
console.log(recents) // ['/path/to/desktop/foo.txt'}
```
### `app.setAsDefaultProtocolClient(protocol[, path, args])` ### `app.setAsDefaultProtocolClient(protocol[, path, args])`
* `protocol` string - The name of your protocol, without `://`. For example, * `protocol` string - The name of your protocol, without `://`. For example,

View file

@ -77,6 +77,11 @@ To clear the list of recent documents, use the
In this guide, the list of documents is cleared once all windows have been In this guide, the list of documents is cleared once all windows have been
closed. closed.
#### Accessing the list of recent documents
To access the list of recent documents, use the
[app.getRecentDocuments][getrecentdocuments] API.
## Additional information ## Additional information
### Windows Notes ### Windows Notes
@ -138,5 +143,6 @@ of `app` module will be emitted for it.
[dock-menu-image]: https://cloud.githubusercontent.com/assets/639601/5069610/2aa80758-6e97-11e4-8cfb-c1a414a10774.png [dock-menu-image]: https://cloud.githubusercontent.com/assets/639601/5069610/2aa80758-6e97-11e4-8cfb-c1a414a10774.png
[addrecentdocument]: ../api/app.md#appaddrecentdocumentpath-macos-windows [addrecentdocument]: ../api/app.md#appaddrecentdocumentpath-macos-windows
[clearrecentdocuments]: ../api/app.md#appclearrecentdocuments-macos-windows [clearrecentdocuments]: ../api/app.md#appclearrecentdocuments-macos-windows
[getrecentdocuments]: ../api/app.md#appgetrecentdocuments-macos-windows
[app-registration]: https://learn.microsoft.com/en-us/windows/win32/shell/app-registration [app-registration]: https://learn.microsoft.com/en-us/windows/win32/shell/app-registration
[menu-item-image]: https://user-images.githubusercontent.com/3168941/33003655-ea601c3a-cd70-11e7-97fa-7c062149cfb1.png [menu-item-image]: https://user-images.githubusercontent.com/3168941/33003655-ea601c3a-cd70-11e7-97fa-7c062149cfb1.png

View file

@ -1720,6 +1720,8 @@ gin::ObjectTemplateBuilder App::GetObjectTemplateBuilder(v8::Isolate* isolate) {
base::BindRepeating(&Browser::AddRecentDocument, browser)) base::BindRepeating(&Browser::AddRecentDocument, browser))
.SetMethod("clearRecentDocuments", .SetMethod("clearRecentDocuments",
base::BindRepeating(&Browser::ClearRecentDocuments, browser)) base::BindRepeating(&Browser::ClearRecentDocuments, browser))
.SetMethod("getRecentDocuments",
base::BindRepeating(&Browser::GetRecentDocuments, browser))
#if BUILDFLAG(IS_WIN) #if BUILDFLAG(IS_WIN)
.SetMethod("setAppUserModelId", .SetMethod("setAppUserModelId",
base::BindRepeating(&Browser::SetAppUserModelID, browser)) base::BindRepeating(&Browser::SetAppUserModelID, browser))

View file

@ -125,6 +125,9 @@ class Browser : private WindowListObserver {
// Clear the recent documents list. // Clear the recent documents list.
void ClearRecentDocuments(); void ClearRecentDocuments();
// Return the recent documents list.
std::vector<std::string> GetRecentDocuments();
#if BUILDFLAG(IS_WIN) #if BUILDFLAG(IS_WIN)
// Set the application user model ID. // Set the application user model ID.
void SetAppUserModelID(const std::wstring& name); void SetAppUserModelID(const std::wstring& name);

View file

@ -95,6 +95,10 @@ bool SetDefaultWebClient(const std::string& protocol) {
void Browser::AddRecentDocument(const base::FilePath& path) {} void Browser::AddRecentDocument(const base::FilePath& path) {}
std::vector<std::string> Browser::GetRecentDocuments() {
return std::vector<std::string>();
}
void Browser::ClearRecentDocuments() {} void Browser::ClearRecentDocuments() {}
bool Browser::SetAsDefaultProtocolClient(const std::string& protocol, bool Browser::SetAsDefaultProtocolClient(const std::string& protocol,

View file

@ -162,19 +162,31 @@ void Browser::Show() {
} }
void Browser::AddRecentDocument(const base::FilePath& path) { void Browser::AddRecentDocument(const base::FilePath& path) {
NSString* path_string = base::apple::FilePathToNSString(path); NSURL* url = base::apple::FilePathToNSURL(path);
if (!path_string) if (!url) {
LOG(WARNING) << "Failed to convert file path " << path.value()
<< " to NSURL";
return; return;
NSURL* u = [NSURL fileURLWithPath:path_string]; }
if (!u)
return; [[NSDocumentController sharedDocumentController]
[[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL:u]; noteNewRecentDocumentURL:url];
} }
void Browser::ClearRecentDocuments() { void Browser::ClearRecentDocuments() {
[[NSDocumentController sharedDocumentController] clearRecentDocuments:nil]; [[NSDocumentController sharedDocumentController] clearRecentDocuments:nil];
} }
std::vector<std::string> Browser::GetRecentDocuments() {
NSArray<NSURL*>* recentURLs =
[[NSDocumentController sharedDocumentController] recentDocumentURLs];
std::vector<std::string> documents;
documents.reserve([recentURLs count]);
for (NSURL* url in recentURLs)
documents.push_back(std::string([url.path UTF8String]));
return documents;
}
bool Browser::RemoveAsDefaultProtocolClient(const std::string& protocol, bool Browser::RemoveAsDefaultProtocolClient(const std::string& protocol,
gin::Arguments* args) { gin::Arguments* args) {
NSString* identifier = [base::apple::MainBundle() bundleIdentifier]; NSString* identifier = [base::apple::MainBundle() bundleIdentifier];

View file

@ -17,6 +17,7 @@
#include "base/base_paths.h" #include "base/base_paths.h"
#include "base/command_line.h" #include "base/command_line.h"
#include "base/file_version_info.h" #include "base/file_version_info.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h" #include "base/files/file_path.h"
#include "base/logging.h" #include "base/logging.h"
#include "base/path_service.h" #include "base/path_service.h"
@ -315,14 +316,33 @@ void GetApplicationInfoForProtocolUsingAssocQuery(
app_display_name, std::move(promise)); app_display_name, std::move(promise));
} }
std::string ResolveShortcut(const base::FilePath& lnk_path) {
std::string target_path;
CComPtr<IShellLink> shell_link;
if (SUCCEEDED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&shell_link)))) {
CComPtr<IPersistFile> persist_file;
if (SUCCEEDED(shell_link->QueryInterface(IID_PPV_ARGS(&persist_file)))) {
if (SUCCEEDED(persist_file->Load(lnk_path.value().c_str(), STGM_READ))) {
WCHAR resolved_path[MAX_PATH];
if (SUCCEEDED(
shell_link->GetPath(resolved_path, MAX_PATH, nullptr, 0))) {
target_path = base::FilePath(resolved_path).MaybeAsASCII();
}
}
}
}
return target_path;
}
void Browser::AddRecentDocument(const base::FilePath& path) { void Browser::AddRecentDocument(const base::FilePath& path) {
CComPtr<IShellItem> item; CComPtr<IShellItem> item;
HRESULT hr = SHCreateItemFromParsingName(path.value().c_str(), nullptr, HRESULT hr = SHCreateItemFromParsingName(path.value().c_str(), nullptr,
IID_PPV_ARGS(&item)); IID_PPV_ARGS(&item));
if (SUCCEEDED(hr)) { if (SUCCEEDED(hr)) {
SHARDAPPIDINFO info; SHARDAPPIDINFO info = {item, GetAppUserModelID()};
info.psi = item;
info.pszAppID = GetAppUserModelID();
SHAddToRecentDocs(SHARD_APPIDINFO, &info); SHAddToRecentDocs(SHARD_APPIDINFO, &info);
} }
} }
@ -331,6 +351,33 @@ void Browser::ClearRecentDocuments() {
SHAddToRecentDocs(SHARD_APPIDINFO, nullptr); SHAddToRecentDocs(SHARD_APPIDINFO, nullptr);
} }
std::vector<std::string> Browser::GetRecentDocuments() {
ScopedAllowBlockingForElectron allow_blocking;
std::vector<std::string> docs;
PWSTR recent_path_ptr = nullptr;
HRESULT hr =
SHGetKnownFolderPath(FOLDERID_Recent, 0, nullptr, &recent_path_ptr);
if (SUCCEEDED(hr) && recent_path_ptr) {
base::FilePath recent_folder(recent_path_ptr);
CoTaskMemFree(recent_path_ptr);
base::FileEnumerator enumerator(recent_folder, /*recursive=*/false,
base::FileEnumerator::FILES,
FILE_PATH_LITERAL("*.lnk"));
for (base::FilePath file = enumerator.Next(); !file.empty();
file = enumerator.Next()) {
std::string resolved_path = ResolveShortcut(file);
if (!resolved_path.empty()) {
docs.push_back(resolved_path);
}
}
}
return docs;
}
void Browser::SetAppUserModelID(const std::wstring& name) { void Browser::SetAppUserModelID(const std::wstring& name) {
electron::SetAppUserModelID(name); electron::SetAppUserModelID(name);
} }

View file

@ -11,6 +11,7 @@ import * as http from 'node:http';
import * as https from 'node:https'; import * as https from 'node:https';
import * as net from 'node:net'; import * as net from 'node:net';
import * as path from 'node:path'; import * as path from 'node:path';
import { setTimeout } from 'node:timers/promises';
import { promisify } from 'node:util'; import { promisify } from 'node:util';
import { collectStreamBody, getResponse } from './lib/net-helpers'; import { collectStreamBody, getResponse } from './lib/net-helpers';
@ -19,6 +20,8 @@ import { closeWindow, closeAllWindows } from './lib/window-helpers';
const fixturesPath = path.resolve(__dirname, 'fixtures'); const fixturesPath = path.resolve(__dirname, 'fixtures');
const isMacOSx64 = process.platform === 'darwin' && process.arch === 'x64';
describe('electron module', () => { describe('electron module', () => {
it('does not expose internal modules to require', () => { it('does not expose internal modules to require', () => {
expect(() => { expect(() => {
@ -356,6 +359,44 @@ describe('app module', () => {
}); });
}); });
// GitHub Actions macOS-13 runners used for x64 seem to have a problem with this test.
ifdescribe(process.platform !== 'linux' && !isMacOSx64)('app.{add|get|clear}RecentDocument(s)', () => {
const tempFiles = [
path.join(fixturesPath, 'foo.txt'),
path.join(fixturesPath, 'bar.txt'),
path.join(fixturesPath, 'baz.txt')
];
afterEach(() => {
app.clearRecentDocuments();
for (const file of tempFiles) {
fs.unlinkSync(file);
}
});
beforeEach(() => {
for (const file of tempFiles) {
fs.writeFileSync(file, 'Lorem Ipsum');
}
});
it('can add a recent document', async () => {
app.addRecentDocument(tempFiles[0]);
await setTimeout(2000);
expect(app.getRecentDocuments()).to.include.members([tempFiles[0]]);
});
it('can clear recent documents', async () => {
app.addRecentDocument(tempFiles[1]);
app.addRecentDocument(tempFiles[2]);
await setTimeout(2000);
expect(app.getRecentDocuments()).to.include.members([tempFiles[1], tempFiles[2]]);
app.clearRecentDocuments();
await setTimeout(2000);
expect(app.getRecentDocuments()).to.deep.equal([]);
});
});
describe('app.relaunch', () => { describe('app.relaunch', () => {
let server: net.Server | null = null; let server: net.Server | null = null;
const socketPath = process.platform === 'win32' ? '\\\\.\\pipe\\electron-app-relaunch' : '/tmp/electron-app-relaunch'; const socketPath = process.platform === 'win32' ? '\\\\.\\pipe\\electron-app-relaunch' : '/tmp/electron-app-relaunch';
@ -553,8 +594,8 @@ describe('app module', () => {
describe('app.badgeCount', () => { describe('app.badgeCount', () => {
const platformIsNotSupported = const platformIsNotSupported =
(process.platform === 'win32') || (process.platform === 'win32') ||
(process.platform === 'linux' && !app.isUnityRunning()); (process.platform === 'linux' && !app.isUnityRunning());
const expectedBadgeCount = 42; const expectedBadgeCount = 42;