From 16bad730112f5e33f41c9f55b515a92fba7e41b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Fri, 3 Sep 2021 02:50:35 +0200 Subject: [PATCH] Change config path Closes #116. --- README.md | 2 +- .../scala/com/kubukoz/next/ConfigLoader.scala | 7 ++-- src/main/scala/com/kubukoz/next/Program.scala | 42 ++++++++++++++++--- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 93897930..090af93e 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ This will create a launcher at `./target/universal/stage/bin/spotify-next`. ## Usage The application requires some configuration (e.g. the client ID for the Spotify Web API). -It's stored in a file at `~/.spotify-next.json`. +It's stored in a file at `$XDG_CONFIG_HOME/spotify-next/config.json` or `$HOME/.config/spotify-next/config.json` (whichever works first). When you first run the application, or if that file is deleted, the application will ask and attempt to create one. The configuration defines the port for the embedded HTTP server used for authentication. The server will only start when the login flow is triggered, and stop afterwards. diff --git a/src/main/scala/com/kubukoz/next/ConfigLoader.scala b/src/main/scala/com/kubukoz/next/ConfigLoader.scala index 82a26420..0326d9a6 100644 --- a/src/main/scala/com/kubukoz/next/ConfigLoader.scala +++ b/src/main/scala/com/kubukoz/next/ConfigLoader.scala @@ -14,6 +14,7 @@ import cats.effect.std.Console import fs2.io.file.Files import cats.FlatMap import cats.MonadThrow +import fs2.Pipe trait ConfigLoader[F[_]] { def saveConfig(config: Config): F[Unit] @@ -58,9 +59,9 @@ object ConfigLoader { def default[F[_]: Files: MonadThrow](configPath: Path)(using fs2.Compiler[F, F]): ConfigLoader[F] = new ConfigLoader[F] { - private val createOrOverwriteFile = - Files[F] - .writeAll(configPath, List(StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) + private val createOrOverwriteFile: Pipe[F, Byte, Nothing] = bytes => + fs2.Stream.exec(Files[F].createDirectories(configPath.getParent).void) ++ + bytes.through(Files[F].writeAll(configPath, List(StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))) def saveConfig(config: Config): F[Unit] = fs2 diff --git a/src/main/scala/com/kubukoz/next/Program.scala b/src/main/scala/com/kubukoz/next/Program.scala index 5c4a8c01..323fae0c 100644 --- a/src/main/scala/com/kubukoz/next/Program.scala +++ b/src/main/scala/com/kubukoz/next/Program.scala @@ -1,6 +1,7 @@ package com.kubukoz.next import cats.effect.Concurrent +import cats.effect.Sync import cats.effect.MonadCancelThrow import cats.effect.Resource import cats.effect.kernel.Async @@ -20,15 +21,44 @@ import org.http4s.client.middleware.RequestLogger import org.http4s.client.middleware.ResponseLogger import java.lang.System import java.nio.file.Paths +import java.nio.file.Path +import cats.data.OptionT +import cats.MonadThrow object Program { - val configPath = Paths.get(System.getProperty("user.home")).resolve(".spotify-next.json") - def makeLoader[F[_]: Files: Ref.Make: UserOutput: Console: fs2.Compiler.Target]: F[ConfigLoader[F]] = - ConfigLoader - .cached[F] - .compose(ConfigLoader.withCreateFileIfMissing[F](configPath)) - .apply(ConfigLoader.default[F](configPath)) + trait System[F[_]] { + def getenv(name: String): F[Option[String]] + } + + object System { + def apply[F[_]](using F: System[F]): System[F] = F + + given [F[_]: Sync]: System[F] = name => Sync[F].delay(java.lang.System.getenv(name)).map(Option(_)) + } + + private def configPath[F[_]: System: MonadThrow]: F[Path] = + OptionT(System[F].getenv("XDG_CONFIG_HOME")) + .map(Paths.get(_)) + .getOrElseF( + System[F] + .getenv("HOME") + .flatMap(_.liftTo[F](new Throwable("HOME not defined, I don't even"))) + .map(Paths.get(_)) + .map(_.resolve(".config")) + ) + .map( + _.resolve("spotify-next") + .resolve("config.json") + ) + + def makeLoader[F[_]: Files: System: Ref.Make: UserOutput: Console: fs2.Compiler.Target]: F[ConfigLoader[F]] = + configPath[F].flatMap { p => + ConfigLoader + .cached[F] + .compose(ConfigLoader.withCreateFileIfMissing[F](p)) + .apply(ConfigLoader.default[F](p)) + } def makeBasicClient[F[_]: Async]: Resource[F, Client[F]] = Resource