KiCad PCB EDA Suite
Loading...
Searching...
No Matches
command_api_server.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 * @author Jon Evans <[email protected]>
6 *
7 * This program is free software: you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License as published by the
9 * Free Software Foundation, either version 3 of the License, or (at your
10 * option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
19 */
20
21#include <csignal>
22#include <atomic>
23#include <algorithm>
24#include <vector>
25
29#include <api/api_utils.h>
30#include <api/api_server.h>
31#include <cli/exit_codes.h>
32#include <lib_id.h>
35#include <wx/app.h>
36#include <wx/crt.h>
37#include <wx/filename.h>
38
39#include "command_api_server.h"
40
41#define ARG_PATH "path"
42#define ARG_SOCKET "--socket"
43
44
45std::atomic_bool g_apiServerExitRequested{ false };
46
48{
49 g_apiServerExitRequested.store( true );
50}
51
52
54 COMMAND( "api-server" )
55{
56 m_argParser.add_description( UTF8STDSTR( _( "Run the KiCad IPC API server in headless mode" ) ) );
57
58 m_argParser.add_argument( ARG_PATH )
59 .default_value( std::string() )
60 .nargs( argparse::nargs_pattern::optional )
61 .help( UTF8STDSTR( _( "Optional path to a .kicad_pro, .kicad_pcb, or .kicad_sch file to pre-load" ) ) )
62 .metavar( "PROJECT_OR_FILE" );
63
64 m_argParser.add_argument( ARG_SOCKET )
65 .default_value( std::string() )
66 .help( UTF8STDSTR( _( "Override API socket path" ) ) )
67 .metavar( "SOCKET_PATH" );
68}
69
70
72{
73 using namespace kiapi::common;
74
75 std::unique_ptr<KICAD_API_SERVER> server = std::make_unique<KICAD_API_SERVER>( false );
76 API_HANDLER_COMMON commonHandler;
77 API_HANDLER_LIBRARIES designBlockLibrariesHandler( LIBRARY_TABLE_TYPE::DESIGN_BLOCK );
78
79 // The design block library handler handles LoadAllLibraries commands which need to be able
80 // to lazy-load the eeschema/pcbnew faces if they aren't loaded
81 designBlockLibrariesHandler.SetKiway( &aKiway );
82 designBlockLibrariesHandler.SetLibraryHandlerRegistrar(
83 [&server]( KIFACE* aKiface )
84 {
85 aKiface->RegisterLibraryHandlers( server.get() );
86 } );
87
88 wxString socketPath = wxString::FromUTF8( m_argParser.get<std::string>( ARG_SOCKET ) );
89
90 if( !socketPath.IsEmpty() )
91 server->SetSocketPath( socketPath );
92
93 // Eventually we might support opening multiple projects at once, but for now
94 // we support one project at a time, but multiple documents within that project
95 // (e.g. up to one schematic, up to one board, and arbitrarily many library files
96 // which are not associated with the project)
97 std::optional<wxFileName> openProjectPath;
98
99 struct OPEN_DOCUMENT
100 {
101 types::DocumentType type;
102 wxString fileName;
103 LIB_ID libId;
104 };
105
106 std::vector<OPEN_DOCUMENT> openDocuments;
107
108 auto faceForDocument = []( types::DocumentType aType ) -> KIWAY::FACE_T
109 {
110 switch( aType )
111 {
112 case types::DOCTYPE_SCHEMATIC: return KIWAY::FACE_SCH;
113 case types::DOCTYPE_PCB: return KIWAY::FACE_PCB;
114 case types::DOCTYPE_FOOTPRINT: return KIWAY::FACE_PCB;
115 default: return KIWAY::KIWAY_FACE_COUNT;
116 }
117 };
118
119 auto closeAllDocuments =
120 [&]( const commands::CloseAllDocuments& aRequest ) -> HANDLER_RESULT<google::protobuf::Empty>
121 {
122 for( const OPEN_DOCUMENT& doc : openDocuments )
123 {
124 // The project has no document face; it is released by UnloadProject below.
125 if( doc.type == types::DOCTYPE_PROJECT )
126 continue;
127
128 wxString error;
129 aKiway.ProcessApiCloseDocument( faceForDocument( doc.type ), doc.fileName, server.get(), &error );
130 }
131
132 openDocuments.clear();
133
134 if( openProjectPath )
135 {
138 }
139
140 openProjectPath.reset();
141
142 return google::protobuf::Empty();
143 };
144
145 auto openDocument = [&]( const commands::OpenDocument& aRequest )
147 {
148 types::DocumentType requestType = aRequest.type();
149
150 if( requestType != types::DOCTYPE_PCB && requestType != types::DOCTYPE_SCHEMATIC
151 && requestType != types::DOCTYPE_PROJECT && requestType != types::DOCTYPE_FOOTPRINT )
152 {
153 ApiResponseStatus e;
154 e.set_status( ApiStatusCode::AS_UNIMPLEMENTED );
155 e.set_error_message( "Only PCB, schematic, footprint, and project document types are supported" );
156 return tl::unexpected( e );
157 }
158
159 wxString inputPath = wxString::FromUTF8( aRequest.path() );
160
161 if( inputPath.IsEmpty() )
162 {
163 ApiResponseStatus e;
164 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
165 e.set_error_message( "OpenDocument requires a non-empty path" );
166 return tl::unexpected( e );
167 }
168
169 if( requestType == types::DOCTYPE_FOOTPRINT )
170 {
171 LIB_ID fpid;
172
173 if( fpid.Parse( inputPath ) >= 0 )
174 {
175 ApiResponseStatus e;
176 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
177 e.set_error_message( wxString::Format( wxS( "Invalid footprint LIB_ID: %s" ),
178 inputPath ).ToStdString() );
179 return tl::unexpected( e );
180 }
181
184 spec.libId = fpid;
185
186 if( openProjectPath )
187 spec.path = openProjectPath->GetFullPath();
188
189 wxString error;
190
191 if( !aKiway.ProcessApiOpenDocument( KIWAY::FACE_PCB, spec, server.get(), &error ) )
192 {
193 ApiResponseStatus e;
194 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
195 e.set_error_message( error.ToStdString() );
196 return tl::unexpected( e );
197 }
198
199 OPEN_DOCUMENT doc;
200 doc.type = requestType;
201 doc.libId = fpid;
202 openDocuments.push_back( doc );
203
204 commands::OpenDocumentResponse response;
205 types::DocumentSpecifier* docSpec = response.mutable_document();
206 docSpec->set_type( requestType );
207 docSpec->mutable_lib_id()->set_library_nickname( fpid.GetUniStringLibNickname() );
208 docSpec->mutable_lib_id()->set_entry_name( fpid.GetUniStringLibItemName() );
209
210 return response;
211 }
212
213 wxFileName projectPath( inputPath );
214 projectPath.SetExt( FILEEXT::ProjectFileExtension );
215 projectPath.MakeAbsolute();
216
217 if( openProjectPath && projectPath.GetFullPath() != openProjectPath->GetFullPath() )
218 {
219 ApiResponseStatus e;
220 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
221 e.set_error_message( wxString::Format( "cannot open a document from project '%s' because project "
222 "'%s' is already open.",
223 projectPath.GetFullName(), openProjectPath->GetFullName() )
224 .ToStdString() );
225 return tl::unexpected( e );
226 }
227
228 if( requestType == types::DOCTYPE_PROJECT )
229 {
230 if( !openProjectPath )
231 {
232 if( !openDocuments.empty() )
233 {
234 auto closeResult = closeAllDocuments( commands::CloseAllDocuments() );
235
236 if( !closeResult )
237 return tl::unexpected( closeResult.error() );
238 }
239
240 if( !Pgm().GetSettingsManager().LoadProject( projectPath.GetFullPath(), true ) )
241 {
242 wxLogTrace( traceApi, "Warning: no project file found for %s", inputPath );
243 }
244
245 if( !Pgm().GetSettingsManager().GetProject( projectPath.GetFullPath() ) )
246 {
247 ApiResponseStatus e;
248 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
249 e.set_error_message( wxString::Format( "failed to load project '%s'", projectPath.GetFullPath() )
250 .ToStdString() );
251 return tl::unexpected( e );
252 }
253
254 openProjectPath = projectPath;
255 }
256
257 if( std::ranges::find_if( openDocuments,
258 []( const OPEN_DOCUMENT& d )
259 {
260 return d.type == types::DOCTYPE_PROJECT;
261 } ) == openDocuments.end() )
262 {
263 OPEN_DOCUMENT doc;
264 doc.type = types::DOCTYPE_PROJECT;
265 doc.fileName = projectPath.GetFullName();
266 openDocuments.push_back( doc );
267 }
268
269 commands::OpenDocumentResponse response;
270 types::DocumentSpecifier* doc = response.mutable_document();
272
273 doc->set_type( types::DOCTYPE_PROJECT );
274 doc->mutable_project()->set_name( project.GetProjectName().ToUTF8() );
275 doc->mutable_project()->set_path( project.GetProjectPath().ToUTF8() );
276
277 return response;
278 }
279
280 KIWAY::FACE_T face = faceForDocument( requestType );
281
282 if( face == KIWAY::KIWAY_FACE_COUNT )
283 {
284 ApiResponseStatus e;
285 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
286 e.set_error_message( "unsupported document type" );
287 return tl::unexpected( e );
288 }
289
290 if( requestType == types::DOCTYPE_PCB || requestType == types::DOCTYPE_SCHEMATIC )
291 {
292 auto existing = std::ranges::find_if( openDocuments,
293 [&]( const OPEN_DOCUMENT& d )
294 {
295 return d.type == requestType;
296 } );
297
298 if( existing != openDocuments.end() )
299 {
300 ApiResponseStatus e;
301 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
302 e.set_error_message( "a document of this type is already open" );
303 return tl::unexpected( e );
304 }
305 }
306
309 spec.path = projectPath.GetFullPath();
310
311 wxString error;
312
313 if( !aKiway.ProcessApiOpenDocument( face, spec, server.get(), &error ) )
314 {
315 ApiResponseStatus e;
316 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
317 e.set_error_message( error.ToStdString() );
318 return tl::unexpected( e );
319 }
320
321 wxFileName docFile( inputPath );
322 docFile.MakeAbsolute();
323
324 OPEN_DOCUMENT doc;
325 doc.type = requestType;
326 doc.fileName = docFile.GetFullName();
327 openDocuments.push_back( doc );
328
329 openProjectPath = projectPath;
330
331 commands::OpenDocumentResponse response;
332 types::DocumentSpecifier* docSpec = response.mutable_document();
334
335 docSpec->set_type( requestType );
336
337 if( requestType == types::DOCTYPE_PCB )
338 docSpec->set_board_filename( doc.fileName.ToStdString() );
339
340 docSpec->mutable_project()->set_name( project.GetProjectName().ToUTF8() );
341 docSpec->mutable_project()->set_path( project.GetProjectPath().ToUTF8() );
342
343 return response;
344 };
345
346 auto createDocument =
347 [&]( const commands::CreateDocument& aRequest ) -> HANDLER_RESULT<commands::OpenDocumentResponse>
348 {
349 types::DocumentType requestType = aRequest.type();
350
351 // TODO could allow creating entire projects in one go
352 // or expose create from template
353 if( requestType != types::DOCTYPE_PCB && requestType != types::DOCTYPE_SCHEMATIC )
354 {
355 ApiResponseStatus e;
356 e.set_status( ApiStatusCode::AS_UNIMPLEMENTED );
357 e.set_error_message( "Only PCB and schematic documents can be created" );
358 return tl::unexpected( e );
359 }
360
361 wxString inputPath = wxString::FromUTF8( aRequest.path() );
362
363 if( inputPath.IsEmpty() )
364 {
365 ApiResponseStatus e;
366 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
367 e.set_error_message( "CreateDocument requires a non-empty path" );
368 return tl::unexpected( e );
369 }
370
371 wxFileName docPath( inputPath );
372 docPath.MakeAbsolute();
373 docPath.SetExt( requestType == types::DOCTYPE_PCB ? FILEEXT::KiCadPcbFileExtension
375
378 spec.path = docPath.GetFullPath();
379
380 KIWAY::FACE_T face = ( requestType == types::DOCTYPE_PCB ) ? KIWAY::FACE_PCB : KIWAY::FACE_SCH;
381 wxString error;
382
383 if( !aKiway.ProcessApiOpenDocument( face, spec, server.get(), &error ) )
384 {
385 ApiResponseStatus e;
386 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
387 e.set_error_message( error.ToStdString() );
388 return tl::unexpected( e );
389 }
390
392 openProjectPath =
393 wxFileName( project.GetProjectPath(), project.GetProjectName(), FILEEXT::ProjectFileExtension );
394
395 OPEN_DOCUMENT doc;
396 doc.type = requestType;
397 doc.fileName = docPath.GetFullName();
398
399 openDocuments.push_back( doc );
400
401 commands::OpenDocumentResponse response;
402 types::DocumentSpecifier* docSpec = response.mutable_document();
403
404 docSpec->set_type( requestType );
405
406 if( requestType == types::DOCTYPE_PCB )
407 docSpec->set_board_filename( doc.fileName.ToStdString() );
408
409 PackProject( *docSpec->mutable_project(), project );
410
411 return response;
412 };
413
414 auto closeDocument =
415 [&]( const commands::CloseDocument& aRequest ) -> HANDLER_RESULT<google::protobuf::Empty>
416 {
417 if( openDocuments.empty() )
418 {
419 ApiResponseStatus e;
420 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
421 e.set_error_message( "No document is currently open" );
422 return tl::unexpected( e );
423 }
424
425 auto it = openDocuments.end();
426
427 if( aRequest.has_document() )
428 {
429 types::DocumentType typeToClose = aRequest.document().type();
430
431 it = std::ranges::find_if( openDocuments,
432 [&]( const OPEN_DOCUMENT& d )
433 {
434 return d.type == typeToClose;
435 } );
436
437 if( it == openDocuments.end() )
438 {
439 ApiResponseStatus e;
440 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
441 e.set_error_message( "Requested document type does not match any open document" );
442 return tl::unexpected( e );
443 }
444
445 if( typeToClose == types::DOCTYPE_PCB
446 && !aRequest.document().board_filename().empty()
447 && it->fileName != wxString::FromUTF8( aRequest.document().board_filename() ) )
448 {
449 ApiResponseStatus e;
450 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
451 e.set_error_message( "Requested document does not match the open document" );
452 return tl::unexpected( e );
453 }
454
455 if( ( typeToClose == types::DOCTYPE_SCHEMATIC || typeToClose == types::DOCTYPE_PROJECT )
456 && aRequest.document().has_project()
457 && openProjectPath
458 && aRequest.document().project().name() != openProjectPath->GetName().ToStdString() )
459 {
460 ApiResponseStatus e;
461 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
462 e.set_error_message( "Requested document does not match the open project" );
463 return tl::unexpected( e );
464 }
465
466 if( typeToClose == types::DOCTYPE_FOOTPRINT && aRequest.document().has_lib_id() )
467 {
468 LIB_ID fpid = UnpackLibId( aRequest.document().lib_id() );
469
470 if( !fpid.IsValid() )
471 {
472 ApiResponseStatus e;
473 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
474 e.set_error_message( wxString::Format( wxS( "Invalid footprint LIB_ID: %s" ),
475 fpid.GetUniStringLibId() ).ToStdString() );
476 return tl::unexpected( e );
477 }
478
479 if( it->libId != fpid )
480 {
481 ApiResponseStatus e;
482 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
483 e.set_error_message( "Requested document does not match the open document" );
484 return tl::unexpected( e );
485 }
486 }
487 }
488 else
489 {
490 // No document specifier: close the first open document.
491 it = openDocuments.begin();
492 }
493
494 if( it->type == types::DOCTYPE_PROJECT )
495 {
496 return closeAllDocuments( commands::CloseAllDocuments() );
497 }
498 else
499 {
500 wxString error;
501
502 if( !aKiway.ProcessApiCloseDocument( faceForDocument( it->type ), it->fileName, server.get(), &error ) )
503 {
504 ApiResponseStatus e;
505 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
506 e.set_error_message( error.ToStdString() );
507 return tl::unexpected( e );
508 }
509 }
510
511 openDocuments.erase( it );
512
513 if( openDocuments.empty() && openProjectPath )
514 {
517 openProjectPath.reset();
518 }
519
520 return google::protobuf::Empty();
521 };
522
523 commonHandler.SetOpenDocumentHandler( openDocument );
524 commonHandler.SetCreateDocumentHandler( createDocument );
525 commonHandler.SetCloseDocumentHandler( closeDocument );
526 commonHandler.SetCloseAllDocumentsHandler( closeAllDocuments );
527
528 server->RegisterHandler( &commonHandler );
529 server->RegisterHandler( &designBlockLibrariesHandler );
530 server->Start();
531
532 if( !server->Running() )
533 {
534 wxFprintf( stderr, _( "Failed to start API server\n" ) );
536 }
537
538 wxString preloadPath = wxString::FromUTF8( m_argParser.get<std::string>( ARG_PATH ) );
539
540 if( !preloadPath.IsEmpty() )
541 {
542 using namespace kiapi::common;
543
544 wxFileName preloadFile( preloadPath );
545 types::DocumentType preloadType = types::DOCTYPE_PROJECT;
546
547 if( preloadFile.GetExt() == FILEEXT::KiCadSchematicFileExtension )
548 preloadType = types::DOCTYPE_SCHEMATIC;
549 else if( preloadFile.GetExt() == FILEEXT::KiCadPcbFileExtension )
550 preloadType = types::DOCTYPE_PCB;
551
552 commands::OpenDocument request;
553 request.set_type( preloadType );
554 request.set_path( preloadPath.ToStdString() );
555
556 auto preloadResult = openDocument( request );
557
558 if( !preloadResult )
559 {
560 wxFprintf( stderr, "%s\n", preloadResult.error().error_message() );
561 server->DeregisterHandler( &commonHandler );
562 server->DeregisterHandler( &designBlockLibrariesHandler );
564 }
565 }
566
567 server->SetReadyToReply( true );
568
569 wxString listenPath = wxString::FromUTF8( server->SocketPath() );
570 wxFprintf( stdout, "KiCad API server listening at %s\n", listenPath );
571
572 auto oldSigInt = std::signal( SIGINT, apiServerSignalHandler );
573#ifdef SIGTERM
574 auto oldSigTerm = std::signal( SIGTERM, apiServerSignalHandler );
575#endif
576
577 g_apiServerExitRequested.store( false );
578
579 while( !g_apiServerExitRequested.load() )
580 {
581 wxTheApp->ProcessPendingEvents();
582 wxMilliSleep( 10 );
583 }
584
585 std::signal( SIGINT, oldSigInt );
586#ifdef SIGTERM
587 std::signal( SIGTERM, oldSigTerm );
588#endif
589
590 wxFprintf( stdout, "Shutting down\n" );
591
592 commands::CloseAllDocuments closeAllReq;
593 closeAllDocuments( closeAllReq );
594 server->DeregisterHandler( &commonHandler );
595 server->DeregisterHandler( &designBlockLibrariesHandler );
596
597 return EXIT_CODES::OK;
598}
tl::expected< T, ApiResponseStatus > HANDLER_RESULT
Definition api_handler.h:45
void SetOpenDocumentHandler(OPEN_DOCUMENT_HANDLER aHandler)
void SetCloseAllDocumentsHandler(CLOSE_ALL_DOCUMENTS_HANDLER aHandler)
void SetCreateDocumentHandler(CREATE_DOCUMENT_HANDLER aHandler)
void SetCloseDocumentHandler(CLOSE_DOCUMENT_HANDLER aHandler)
Base class for API handlers related to library management.
void SetKiway(KIWAY *aKiway)
void SetLibraryHandlerRegistrar(std::function< void(KIFACE *)> aRegistrar)
Set a callback to register the handler on the API server in headless mode.
int doPerform(KIWAY &aKiway) override
The internal handler that should be overloaded to implement command specific processing and work.
argparse::ArgumentParser m_argParser
Definition command.h:113
COMMAND(const std::string &aName)
Define a new COMMAND instance.
Definition command.cpp:30
A minimalistic software bus for communications between various DLLs/DSOs (DSOs) within the same KiCad...
Definition kiway.h:340
bool ProcessApiCloseDocument(KIWAY::FACE_T aFace, const wxString &aPath, KICAD_API_SERVER *aServer, wxString *aError=nullptr)
Definition kiway.cpp:773
bool ProcessApiOpenDocument(KIWAY::FACE_T aFace, const KIFACE::DOCUMENT_SPEC &aSpec, KICAD_API_SERVER *aServer, wxString *aError=nullptr)
Definition kiway.cpp:756
FACE_T
Known KIFACE implementations.
Definition kiway.h:346
@ KIWAY_FACE_COUNT
Definition kiway.h:355
@ FACE_SCH
eeschema DSO
Definition kiway.h:347
@ FACE_PCB
pcbnew DSO
Definition kiway.h:348
A logical library item identifier and consists of various portions much like a URI.
Definition lib_id.h:45
int Parse(const UTF8 &aId, bool aFix=false)
Parse LIB_ID with the information from aId.
Definition lib_id.cpp:65
bool IsValid() const
Check if this LID_ID is valid.
Definition lib_id.h:168
wxString GetUniStringLibId() const
Definition lib_id.h:144
const wxString GetUniStringLibItemName() const
Get strings for display messages in dialogs.
Definition lib_id.h:108
const wxString GetUniStringLibNickname() const
Definition lib_id.h:84
virtual SETTINGS_MANAGER & GetSettingsManager() const
Definition pgm_base.h:123
Container for project specific data.
Definition project.h:63
bool UnloadProject(PROJECT *aProject, bool aSave=true)
Save, unload and unregister the given PROJECT.
PROJECT & Prj() const
A helper while we are not MDI-capable – return the one and only project.
#define UTF8STDSTR(s)
Definition command.h:27
std::atomic_bool g_apiServerExitRequested
#define ARG_PATH
void apiServerSignalHandler(int)
#define ARG_SOCKET
static std::string ToStdString(const wxString &aStr)
#define _(s)
static const std::string ProjectFileExtension
static const std::string KiCadSchematicFileExtension
static const std::string KiCadPcbFileExtension
const wxChar *const traceApi
Flag to enable debug output related to the IPC API and its plugin system.
Definition api_utils.cpp:33
static const int ERR_ARGS
Definition exit_codes.h:31
static const int OK
Definition exit_codes.h:30
static const int ERR_UNKNOWN
Definition exit_codes.h:32
KICOMMON_API void PackProject(types::ProjectSpecifier &aOutput, const PROJECT &aInput)
KICOMMON_API LIB_ID UnpackLibId(const types::LibraryIdentifier &aId)
PGM_BASE & Pgm()
The global program "get" accessor.
wxString path
File path (KIND::FILE_KIND) or project path (KIND::FPID_KIND).
Definition kiway.h:164
LIB_ID libId
Library identifier; valid when kind == KIND::FPID_KIND.
Definition kiway.h:165
@ FPID_KIND
Open the library element named by libId.
Definition kiway.h:159
@ FILE_KIND
Open the file at path.
Definition kiway.h:158
@ CREATE_KIND
Create a new document at path and open it (in memory, not persisted)
Definition kiway.h:160
Implement a participant in the KIWAY alchemy.
Definition kiway.h:153
virtual void RegisterLibraryHandlers(KICAD_API_SERVER *aServer)
Register this face's library API handlers on the given server.
Definition kiway.h:301
Definition of file extensions used in Kicad.