{"id":26964450,"url":"https://github.com/nicklayb/snowhite","last_synced_at":"2025-07-14T08:32:50.265Z","repository":{"id":44898680,"uuid":"292135472","full_name":"nicklayb/snowhite","owner":"nicklayb","description":"Smart mirror application written in Elixir for better concurrency and availability","archived":false,"fork":false,"pushed_at":"2024-06-04T13:08:53.000Z","size":460,"stargazers_count":8,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-03T06:37:29.495Z","etag":null,"topics":["concurrent","mirror","smart-mirror"],"latest_commit_sha":null,"homepage":"https://github.com/nicklayb/snowhite","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nicklayb.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-09-02T00:11:08.000Z","updated_at":"2025-02-23T17:54:53.000Z","dependencies_parsed_at":"2025-04-03T06:42:03.353Z","dependency_job_id":null,"html_url":"https://github.com/nicklayb/snowhite","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/nicklayb/snowhite","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nicklayb%2Fsnowhite","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nicklayb%2Fsnowhite/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nicklayb%2Fsnowhite/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nicklayb%2Fsnowhite/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nicklayb","download_url":"https://codeload.github.com/nicklayb/snowhite/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nicklayb%2Fsnowhite/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265262688,"owners_count":23736448,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["concurrent","mirror","smart-mirror"],"created_at":"2025-04-03T06:31:34.028Z","updated_at":"2025-07-14T08:32:50.233Z","avatar_url":"https://github.com/nicklayb.png","language":"Elixir","funding_links":["https://ko-fi.com/D1D2YX9OU"],"categories":[],"sub_categories":[],"readme":"# Snowhite\n\n\u003e Mirror mirror, tell me who is the most beautiful\n\n![Snowhite demo](demo.gif \"Snowhite demo\")\n\n[Blog article about Snowhite](https://nboisvert.com/blog/so-i-built-a-smart-mirror-using-elixir)\n\n## Fetching deps\n\nYou need to fetch both node and elixir deps before playing with it. You can use the `make deps` target to do so.\n\n## Initial setup\n\n### Creating the project\nCreate a new phoenix project\n\n```bash\nmix phx.new my_mirror --no-ecto\n```\n\n*If you want to use Ecto, you can, but Snowhite doesn't require it.*\n\n### Creating the profiles\n\nSnowhite makes use of different profile to better split information. You can create multiple profile with multiple modules for different use cases. You first need to create the profile manager like the following. You **must** at least include a `:default` profile.\n\nWe recommend that you pass in your timezone so the scheduler runs within your timezone. It fallbacks to UTC but if you want to use, for instance, the module Suntime, it might run at weird time instead of 1h am if you do not set your timezone. Unless, of course, if you live in the middle of the planet.\n\n```elixir\ndefmodule MyMirror.Profiles do\n  use Snowhite, timezone: \"America/Toronto\"\n\n  profile(:default, MyMirror.Profiles.Default)\nend\n```\n\nYou can create as much profile as you want as long as their name differs. You can switch between profile using either a param `?profile=another` or a `X-Snowhite-Profile: another` header. It would then load the `:another` profile instead of `:default`\n\n#### Creating a profile and registering modules\n\nYou can register any module in the Profile module using the `register_module/3` macro.\n\n```elixir\ndefmodule MyMirror.Profiles.Default do\n  use Snowhite.Builder.Profile\n\n  configure(\n    locale: \"fr\"\n  )\n\n  register_module(:top_left, Snowhite.Modules.Clock) # will be french\n  register_module(:top_left, Snowhite.Modules.Clock, locale: \"en\") # will be english\nend\n```\n\nIt expects:\n- A position; any of `[top|middle|bottom]_[left|center|right]` (ex. `top_left`, `bottom_right`, etc...)\n- A module using either a `Snowhite.Builder.Module` or `Phoenix.LiveView`; Some examples modules are available in `lib/modules` [see here for custom modules](#creating-modules).\n- A keyword list of options specific to module.\n\n### Route the profiles\n\nIn your Phoenix's router, add the following call\n\n```elixir\ndefmodule SnowhiteNboisvertWeb.Router do\n  #...\n  import Snowhite, only: [snowhite_router: 1]\n\n  pipeline :browser do\n    plug :accepts, [\"html\"]\n    plug :fetch_session\n    plug :fetch_flash\n    plug :protect_from_forgery\n    plug :put_secure_browser_headers\n  end\n\n  pipe_through :browser # Make sure you piped through the browser pipeline\n\n  snowhite_router(SnowhiteNboisvert.Profiles)\nend\n```\n\nSnowhite renders on `/`, so if you want to scope it under `/mirror`, for instance, you can do the following\n\n```elixir\nscope \"/mirror\" do\n  snowhite_router(SnowhiteNboisvert.Profiles)\nend\n```\n\n### Assets\n\nSnowhite requires at least a js file located at `../deps/snowhite/assets/js/live.js`. Even though it does **not** require a CSS file, you might want to import the one provided with Snowhite to have basic styling at `../deps/snowhite/assets/css/app.scss`.\n\n## Creating modules\n\nYou can create your own modules using either the `Snowhite.Builder.Module` or a any raw `Phoenix.LiveView` component.\n\nFor most use case, you might prefer to use the `Snowhite.Builder.Module` as it includes [some convenient functions](#convenient-functions). To use Phoenix.LiveView, refer to [the documentation](https://hexdocs.pm/phoenix_live_view).\n\nThe only required function is `render/1` as shown below.\n\n```elixir\ndefmodule Snowhite.Modules.HelloWorld do\n  use Snowhite.Builder.Module\n\n  def render(assigns) do\n    ~L\"\"\"\n      \u003ch1\u003eHello world.\u003c/h1\u003e\n    \"\"\"\n  end\nend\n```\n\n### Other assigns\n\nIf you need other assigns in your module, you must override `mount/1` to define those. That function recieves a socket with `options` and `params` assigned.\n\n```elixir\ndefmodule Snowhite.Modules.HelloWorld do\n  use Snowhite.Builder.Module\n\n  def mount(socket) do\n    assign(socket, :message, \"Hello, you weird person\")\n  end\n\n  def render(assigns) do\n    ~L\"\"\"\n      \u003ch1\u003e@message\u003c/h1\u003e\n    \"\"\"\n  end\nend\n```\n\n### Using options\n\nYou must first defined supported options like the following\n\n```elixir\ndefmodule Snowhite.Modules.HelloWorld do\n  use Snowhite.Builder.Module\n\n  def module_options do\n    %{\n      message: :required, # Raises if :message option is missing\n      color: {:optional, \"white\"} # Sets \"white\" if :color is missing\n    }\n  end\n\n  # ...\nend\n```\n\nYou can then pass in options like below\n\n```elixir\ndefmodule MyMirror.Profiles.Default do\n  use Snowhite.Builder.Profile\n\n  register_module(:top_left, Snowhite.Modules.HelloWorld, message: \"Hello, punk.\")\nend\n```\n\nAnd access those options in the socket assigns under the `options` key.\n\n```elixir\ndefmodule Snowhite.Modules.HelloWorld do\n  use Snowhite.Builder.Module\n\n  def module_options do\n    %{\n      message: :required,\n      color: {:optional, \"white\"}\n    }\n  end\n\n  def render(%{options: %{color: color, message: message}} = assigns) do\n    ~L\"\"\"\n      \u003ch1 style=\"color: \u003c%= color %\u003e\"\u003e\u003c%= message %\u003e\u003c/h1\u003e\n    \"\"\"\n  end\nend\n```\n\n#### Raises for bad options\n\nIf you were to provide an unsupported option to a module, it would raise. This is an expected behaviour as it could help spotting typos or unintended option passing.\n\n```elixir\ndefmodule Snowhite.Modules.HelloWorld do\n  use Snowhite.Builder.Module\n\n  def module_options do\n    %{\n      message: :required, # Raises if :message option is missing\n      color: {:optional, \"white\"} # Sets \"white\" if :color is missing\n    }\n  end\n\n  # ...\nend\n\ndefmodule MyMirror.Profiles.Default do\n  use Snowhite.Builder.Profile\n\n  register_module(:top_left, Snowhite.Modules.HelloWorld, message: \"Hello, punk.\", locale: \"fr\")\nend\n```\n\nIn this example, an exception would raise saying that `{:locale, \"fr\"}` is not supported as option.\n\n### Data source\n\nEven though it can work perfectly, we recommend creating genserver to keep/update the data your module will use. Mainly because opening 3 instances of the same Snowhite app will use one central source of truth but also because they will be kept in perfect sync. If you keep the data in the live view, opening three instances of the same module will load three times the same data and will be out of sync unless you are able to start the three of the at the e x a c t same time.\n\nDefault modules in Snowhite all includes a server, take a look to have a better understanding of it.\n\n### Convenient functions\n\n#### Periodically sending events\n\nThe best way of working with periodic events is to add a Server that implements a GenServer to your module. Doing so will ensure that all instances of the app are sharing the exact same data and prevent visual failure as they will occur in the server. (See existing modules as inspiration)\n\nHowerver, some modules might require some refresh/update at some point. To do so, you have access to the following helpers:\n\n- `every(ms, name, func)`: Will run a function every `ms` milliseconds under the event `name`. **Note**: Every `name` must be unique as it refers to `handle_info/2` event name. If you want your module to have a configurable scheduled event, you can pass an atom instead of an integer for the `ms`. It will fetch the given option key from the assign instead of using an hardcoded timing\n\n**Bonus**: To write miliseconds in a readable way, there is a sigil `~d` that helps you write clocks. It supports hours, minutes and seconds in the following format\n```elixir\n~d(1h) # 3_600_000\n~d(1m) # 60_000\n~d(1s) # 1_000\n~d(6h30m1s) # 23_401_000\n```\n\n### CSS\n\nYou might need some css to make this beautiful. To do so, create a file under `assets/css/modules/my_module.scss` and register it in `assets/css/modules/_modules.scss`. Now all you need is to fulfill the file.\n\nThe module is scoped under a div that has the module name as class. If your module is named `Snowhite.Modules.SomeNice.Module`, the class will be `snowhite-modules-somenice-module`. It is recommended that you scope all your styling under this. If you want to override anything else (colors, layout etc...), edit it inside over `_override.scss`.\n\n## Start dev server\n\nWe use `direnv` to setup environment. So create or edit `~/.envrc` to add env variable if you need to prefixed with `SNOWHITE_`.\n\nExample to override `PORT`, you add `export SNOWHITE_PORT=1234` in `~/.envrc` and you server will be exposed under `1234`. (**Note**: Any change require server restart)\n\nStart the dev server using `make dev`\n\n## TODO\n\n- [] Write tests\n- [] Extract OpenWeather client to it's own package\n- [] Improve documentations\n- [] Write guides\n- [] Support multiple application names so we can have two clock with different city, for instance.\n\n[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/D1D2YX9OU)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnicklayb%2Fsnowhite","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnicklayb%2Fsnowhite","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnicklayb%2Fsnowhite/lists"}