Skip to content

Commit e7496a8

Browse files
committed
Linux: Save Bookmarks under XDG_STATE_HOME
An installation path like "/usr/share/tangerine/" might be read-only. Define the CPP macro `TANGERINE_SELF_CONTAINED` to use a path relative to the executable, instead. Currently, Windows always uses `TANGERINE_SELF_CONTAINED`, but support for both options could be useful there, too.
1 parent 24cfebf commit e7496a8

File tree

4 files changed

+159
-13
lines changed

4 files changed

+159
-13
lines changed

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ include(GNUInstallDirs)
1515

1616
option(EMBED_LUA "Embed Lua support" ON)
1717
option(EMBED_RACKET "Embed Racket support" OFF)
18+
option(SELF_CONTAINED "Use directory tree instead of XDG_STATE_HOME" OFF)
1819

1920
set(INSTALL_PKG_SUBPATH "tangerine" CACHE PATH
2021
"Subdirectory to form PKGDATADIR from DATADIR")
@@ -149,6 +150,7 @@ set_target_properties(tangerine
149150
RUNTIME_OUTPUT_DIRECTORY
150151
$<PATH:APPEND,${CMAKE_BINARY_DIR},${CMAKE_INSTALL_BINDIR}>)
151152
target_compile_definitions(tangerine PRIVATE
153+
$<$<BOOL:${SELF_CONTAINED}>:"TANGERINE_SELF_CONTAINED">
152154
"TANGERINE_PKGDATADIR_FROM_BINDIR=${PKGDATADIR_FROM_BINDIR}")
153155
target_link_libraries(tangerine PRIVATE
154156
fmt

tangerine/installation.cpp

Lines changed: 140 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,24 @@
1717
#include "whereami.h"
1818
#include <iostream>
1919

20+
#if !_WIN64
21+
#include <unistd.h>
22+
#include <sys/types.h>
23+
#include <pwd.h>
24+
#include <cerrno>
25+
#include <cstring>
26+
const char *const TangerineAppID =
27+
// TODO: consider a reverse-dns name with the escaping recommended in:
28+
// https://docs.gtk.org/gio/type_func.Application.id_is_valid.html
29+
// See rationale in: https://docs.gtk.org/gtk4/migrating-3to4.html#set-a-proper-application-id
30+
"tangerine";
31+
std::optional<std::filesystem::path> GetXDGStateHome();
32+
#endif
33+
2034

2135
StatusCode TangerinePaths::PopulateInstallationPaths()
2236
{
37+
std::filesystem::path ExecutablePath;
2338
{
2439
int Length = wai_getExecutablePath(NULL, 0, NULL);
2540
if (Length > -1)
@@ -44,20 +59,142 @@ StatusCode TangerinePaths::PopulateInstallationPaths()
4459
}
4560
}
4661

47-
ExecutableDir = ExecutablePath.parent_path();
62+
std::filesystem::path ExecutableDir = ExecutablePath.parent_path();
4863

4964
#ifdef TANGERINE_PKGDATADIR_FROM_BINDIR
5065
#define STRINGIFY(x) #x
5166
#define EXPAND_AS_STR(x) STRINGIFY(x)
52-
PkgDataDir = ExecutableDir / std::filesystem::path(EXPAND_AS_STR(TANGERINE_PKGDATADIR_FROM_BINDIR));
67+
std::filesystem::path PkgDataDir = ExecutableDir / std::filesystem::path(EXPAND_AS_STR(TANGERINE_PKGDATADIR_FROM_BINDIR));
5368
#undef EXPAND_AS_STR
5469
#undef STRINGIFY
5570
#else
56-
PkgDataDir = ExecutableDir;
71+
std::filesystem::path PkgDataDir = ExecutableDir;
5772
#endif
5873

5974
ShadersDir = PkgDataDir / std::filesystem::path("shaders");
6075
ModelsDir = PkgDataDir / std::filesystem::path("models");
6176

77+
#if defined(TANGERINE_SELF_CONTAINED)
78+
BookmarksPath = PkgDataDir / std::filesystem::path("bookmarks.txt");
79+
#elif !_WIN64
80+
if (std::optional<std::filesystem::path> HomeDir = GetXDGStateHome())
81+
{
82+
BookmarksPath = HomeDir.value() / std::filesystem::path(TangerineAppID) / std::filesystem::path("bookmarks.txt");
83+
}
84+
else
85+
{
86+
BookmarksPath = std::nullopt;
87+
}
88+
#else // Shouldn't get here: handled in "installation.h". Using %APPDATA% / CSIDL_APPDATA / FOLDERID_RoamingAppData might be useful, though.
89+
# error "Windows currently requires TANGERINE_SELF_CONTAINED."
90+
#endif
91+
6292
return StatusCode::PASS;
6393
}
94+
95+
96+
#if !_WIN64
97+
std::optional<std::filesystem::path> GetHomeDir() {
98+
// Based on `rktio_expand_user_tilde()`.
99+
// License: (Apache-2.0 OR MIT)
100+
101+
// $HOME overrides everything.
102+
if (const char *Home = std::getenv("HOME"))
103+
{
104+
return std::filesystem::path(Home);
105+
}
106+
107+
// $USER and $LOGNAME (in that order) override `getuid()`.
108+
const char *AltUserVar = "USER";
109+
const char *AltUser = std::getenv(AltUserVar);
110+
if (!AltUser)
111+
{
112+
AltUserVar = "LOGNAME";
113+
AltUser = std::getenv(AltUserVar);
114+
}
115+
116+
/* getpwnam(3) man page says: "If one wants to check errno after the
117+
call, it should be set to zero before the call." */
118+
errno = 0;
119+
struct passwd *Passwd = AltUser ? getpwnam(AltUser) : getpwuid(getuid());
120+
int PasswdError = errno;
121+
122+
// Did we find it?
123+
if (Passwd && Passwd->pw_dir)
124+
{
125+
if (0 != PasswdError)
126+
{
127+
std::cout << "Warning: Found home directory, but " << (AltUser ? "getpwnam" : "getpwuid") << " reported an error.\n";
128+
}
129+
else
130+
{
131+
// No warning
132+
return std::filesystem::path(Passwd->pw_dir);
133+
}
134+
}
135+
else if (Passwd)
136+
{
137+
std::cout << "Warning: User exists, but does not have a home directory.\n";
138+
}
139+
else
140+
{
141+
std::cout << "Warning: Could not find home directory: user not found.\n";
142+
}
143+
144+
// Add warning details:
145+
// Was `getuid()` overridden?
146+
if (AltUser)
147+
{
148+
std::cout << " user: " << AltUser << " (from $" << AltUserVar << ");\n";
149+
}
150+
// Report system error.
151+
if (0 != PasswdError)
152+
{
153+
std::cout << " error: " << std::strerror(PasswdError) << "\n";
154+
std::cout << " errno: " << PasswdError << "\n";
155+
}
156+
else
157+
{
158+
std::cout << " errno: not set by " << (AltUser ? "getpwnam" : "getpwuid") << "\n";
159+
}
160+
161+
if (Passwd && Passwd->pw_dir)
162+
{
163+
return std::filesystem::path(Passwd->pw_dir);
164+
}
165+
else
166+
{
167+
return {};
168+
}
169+
}
170+
171+
std::optional<std::filesystem::path> GetXDGStateHome()
172+
{
173+
// Based on `rktio_system_path()`.
174+
// License: (Apache-2.0 OR MIT)
175+
176+
const char *EnvVar = "XDG_STATE_HOME";
177+
const char *DefaultSubpath = ".local/state";
178+
179+
// Check the environment variable.
180+
if (const char *FromEnv = std::getenv(EnvVar))
181+
{
182+
std::filesystem::path Candidate = std::filesystem::path(FromEnv);
183+
// We must ignore the environment variable if it is not an absolute path.
184+
if (Candidate.is_absolute()) {
185+
return Candidate;
186+
}
187+
}
188+
189+
190+
// Environment variable was unset or is invalid.
191+
if (std::optional<std::filesystem::path> Home = GetHomeDir())
192+
{
193+
return Home.value() / std::filesystem::path(DefaultSubpath);
194+
}
195+
else
196+
{
197+
return {};
198+
}
199+
}
200+
#endif /* !_WIN64 */

tangerine/installation.h

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,21 @@
1515

1616
#pragma once
1717
#include <filesystem>
18+
#include <optional>
1819
#include "embedding.h"
1920
#include "errors.h"
2021

22+
#ifndef TANGERINE_SELF_CONTAINED
23+
# ifdef _WIN64
24+
# define TANGERINE_SELF_CONTAINED
25+
# endif
26+
#endif
2127

2228
struct TangerinePaths
2329
{
2430
StatusCode PopulateInstallationPaths();
2531

26-
std::filesystem::path ExecutablePath;
27-
std::filesystem::path ExecutableDir;
28-
std::filesystem::path PkgDataDir;
2932
std::filesystem::path ShadersDir;
3033
std::filesystem::path ModelsDir;
34+
std::optional<std::filesystem::path> BookmarksPath;
3135
};

tangerine/tangerine.cpp

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1747,11 +1747,10 @@ void RenderUI(SDL_Window* Window, bool& Live)
17471747

17481748
void LoadBookmarks()
17491749
{
1750-
std::filesystem::path BookmarksPath =
1751-
// FIXME might be read-only
1752-
Installed.ExecutableDir / "bookmarks.txt";
1753-
if (std::filesystem::is_regular_file(BookmarksPath))
1750+
std::optional<std::filesystem::path> MaybeBookmarksPath = Installed.BookmarksPath;
1751+
if (MaybeBookmarksPath && std::filesystem::is_regular_file(MaybeBookmarksPath.value()))
17541752
{
1753+
std::filesystem::path BookmarksPath = MaybeBookmarksPath.value();
17551754
std::ifstream BookmarksFile;
17561755
BookmarksFile.open(BookmarksPath);
17571756
std::string Bookmark;
@@ -1772,12 +1771,16 @@ void LoadBookmarks()
17721771

17731772
void SaveBookmarks()
17741773
{
1775-
std::filesystem::path BookmarksPath =
1776-
// FIXME might be read-only
1777-
Installed.ExecutableDir / "bookmarks.txt";
1774+
std::optional<std::filesystem::path> MaybeBookmarksPath = Installed.BookmarksPath;
1775+
if (!MaybeBookmarksPath)
1776+
{
1777+
return;
1778+
}
1779+
std::filesystem::path BookmarksPath = MaybeBookmarksPath.value();
17781780
const std::vector<std::string>& Bookmarks = ifd::FileDialog::Instance().GetFavorites();
17791781
if (Bookmarks.size() > 0)
17801782
{
1783+
std::filesystem::create_directories(BookmarksPath.parent_path());
17811784
std::ofstream BookmarksFile;
17821785
BookmarksFile.open(BookmarksPath);
17831786
for (const std::string& Bookmark : Bookmarks)

0 commit comments

Comments
 (0)