Skip to content

Commit

Permalink
Linux: Save Bookmarks under XDG_STATE_HOME
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
LiberalArtist committed Feb 26, 2023
1 parent 24cfebf commit e7496a8
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 13 deletions.
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ include(GNUInstallDirs)

option(EMBED_LUA "Embed Lua support" ON)
option(EMBED_RACKET "Embed Racket support" OFF)
option(SELF_CONTAINED "Use directory tree instead of XDG_STATE_HOME" OFF)

set(INSTALL_PKG_SUBPATH "tangerine" CACHE PATH
"Subdirectory to form PKGDATADIR from DATADIR")
Expand Down Expand Up @@ -149,6 +150,7 @@ set_target_properties(tangerine
RUNTIME_OUTPUT_DIRECTORY
$<PATH:APPEND,${CMAKE_BINARY_DIR},${CMAKE_INSTALL_BINDIR}>)
target_compile_definitions(tangerine PRIVATE
$<$<BOOL:${SELF_CONTAINED}>:"TANGERINE_SELF_CONTAINED">
"TANGERINE_PKGDATADIR_FROM_BINDIR=${PKGDATADIR_FROM_BINDIR}")
target_link_libraries(tangerine PRIVATE
fmt
Expand Down
143 changes: 140 additions & 3 deletions tangerine/installation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,24 @@
#include "whereami.h"
#include <iostream>

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


StatusCode TangerinePaths::PopulateInstallationPaths()
{
std::filesystem::path ExecutablePath;
{
int Length = wai_getExecutablePath(NULL, 0, NULL);
if (Length > -1)
Expand All @@ -44,20 +59,142 @@ StatusCode TangerinePaths::PopulateInstallationPaths()
}
}

ExecutableDir = ExecutablePath.parent_path();
std::filesystem::path ExecutableDir = ExecutablePath.parent_path();

#ifdef TANGERINE_PKGDATADIR_FROM_BINDIR
#define STRINGIFY(x) #x
#define EXPAND_AS_STR(x) STRINGIFY(x)
PkgDataDir = ExecutableDir / std::filesystem::path(EXPAND_AS_STR(TANGERINE_PKGDATADIR_FROM_BINDIR));
std::filesystem::path PkgDataDir = ExecutableDir / std::filesystem::path(EXPAND_AS_STR(TANGERINE_PKGDATADIR_FROM_BINDIR));
#undef EXPAND_AS_STR
#undef STRINGIFY
#else
PkgDataDir = ExecutableDir;
std::filesystem::path PkgDataDir = ExecutableDir;
#endif

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

#if defined(TANGERINE_SELF_CONTAINED)
BookmarksPath = PkgDataDir / std::filesystem::path("bookmarks.txt");
#elif !_WIN64
if (std::optional<std::filesystem::path> HomeDir = GetXDGStateHome())
{
BookmarksPath = HomeDir.value() / std::filesystem::path(TangerineAppID) / std::filesystem::path("bookmarks.txt");
}
else
{
BookmarksPath = std::nullopt;
}
#else // Shouldn't get here: handled in "installation.h". Using %APPDATA% / CSIDL_APPDATA / FOLDERID_RoamingAppData might be useful, though.
# error "Windows currently requires TANGERINE_SELF_CONTAINED."
#endif

return StatusCode::PASS;
}


#if !_WIN64
std::optional<std::filesystem::path> GetHomeDir() {
// Based on `rktio_expand_user_tilde()`.
// License: (Apache-2.0 OR MIT)

// $HOME overrides everything.
if (const char *Home = std::getenv("HOME"))
{
return std::filesystem::path(Home);
}

// $USER and $LOGNAME (in that order) override `getuid()`.
const char *AltUserVar = "USER";
const char *AltUser = std::getenv(AltUserVar);
if (!AltUser)
{
AltUserVar = "LOGNAME";
AltUser = std::getenv(AltUserVar);
}

/* getpwnam(3) man page says: "If one wants to check errno after the
call, it should be set to zero before the call." */
errno = 0;
struct passwd *Passwd = AltUser ? getpwnam(AltUser) : getpwuid(getuid());
int PasswdError = errno;

// Did we find it?
if (Passwd && Passwd->pw_dir)
{
if (0 != PasswdError)
{
std::cout << "Warning: Found home directory, but " << (AltUser ? "getpwnam" : "getpwuid") << " reported an error.\n";
}
else
{
// No warning
return std::filesystem::path(Passwd->pw_dir);
}
}
else if (Passwd)
{
std::cout << "Warning: User exists, but does not have a home directory.\n";
}
else
{
std::cout << "Warning: Could not find home directory: user not found.\n";
}

// Add warning details:
// Was `getuid()` overridden?
if (AltUser)
{
std::cout << " user: " << AltUser << " (from $" << AltUserVar << ");\n";
}
// Report system error.
if (0 != PasswdError)
{
std::cout << " error: " << std::strerror(PasswdError) << "\n";
std::cout << " errno: " << PasswdError << "\n";
}
else
{
std::cout << " errno: not set by " << (AltUser ? "getpwnam" : "getpwuid") << "\n";
}

if (Passwd && Passwd->pw_dir)
{
return std::filesystem::path(Passwd->pw_dir);
}
else
{
return {};
}
}

std::optional<std::filesystem::path> GetXDGStateHome()
{
// Based on `rktio_system_path()`.
// License: (Apache-2.0 OR MIT)

const char *EnvVar = "XDG_STATE_HOME";
const char *DefaultSubpath = ".local/state";

// Check the environment variable.
if (const char *FromEnv = std::getenv(EnvVar))
{
std::filesystem::path Candidate = std::filesystem::path(FromEnv);
// We must ignore the environment variable if it is not an absolute path.
if (Candidate.is_absolute()) {
return Candidate;
}
}


// Environment variable was unset or is invalid.
if (std::optional<std::filesystem::path> Home = GetHomeDir())
{
return Home.value() / std::filesystem::path(DefaultSubpath);
}
else
{
return {};
}
}
#endif /* !_WIN64 */
10 changes: 7 additions & 3 deletions tangerine/installation.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,21 @@

#pragma once
#include <filesystem>
#include <optional>
#include "embedding.h"
#include "errors.h"

#ifndef TANGERINE_SELF_CONTAINED
# ifdef _WIN64
# define TANGERINE_SELF_CONTAINED
# endif
#endif

struct TangerinePaths
{
StatusCode PopulateInstallationPaths();

std::filesystem::path ExecutablePath;
std::filesystem::path ExecutableDir;
std::filesystem::path PkgDataDir;
std::filesystem::path ShadersDir;
std::filesystem::path ModelsDir;
std::optional<std::filesystem::path> BookmarksPath;
};
17 changes: 10 additions & 7 deletions tangerine/tangerine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1747,11 +1747,10 @@ void RenderUI(SDL_Window* Window, bool& Live)

void LoadBookmarks()
{
std::filesystem::path BookmarksPath =
// FIXME might be read-only
Installed.ExecutableDir / "bookmarks.txt";
if (std::filesystem::is_regular_file(BookmarksPath))
std::optional<std::filesystem::path> MaybeBookmarksPath = Installed.BookmarksPath;
if (MaybeBookmarksPath && std::filesystem::is_regular_file(MaybeBookmarksPath.value()))
{
std::filesystem::path BookmarksPath = MaybeBookmarksPath.value();
std::ifstream BookmarksFile;
BookmarksFile.open(BookmarksPath);
std::string Bookmark;
Expand All @@ -1772,12 +1771,16 @@ void LoadBookmarks()

void SaveBookmarks()
{
std::filesystem::path BookmarksPath =
// FIXME might be read-only
Installed.ExecutableDir / "bookmarks.txt";
std::optional<std::filesystem::path> MaybeBookmarksPath = Installed.BookmarksPath;
if (!MaybeBookmarksPath)
{
return;
}
std::filesystem::path BookmarksPath = MaybeBookmarksPath.value();
const std::vector<std::string>& Bookmarks = ifd::FileDialog::Instance().GetFavorites();
if (Bookmarks.size() > 0)
{
std::filesystem::create_directories(BookmarksPath.parent_path());
std::ofstream BookmarksFile;
BookmarksFile.open(BookmarksPath);
for (const std::string& Bookmark : Bookmarks)
Expand Down

0 comments on commit e7496a8

Please sign in to comment.