diff --git a/CHANGELOG.md b/CHANGELOG.md index 74a63e8..e9f49ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,36 +1,23 @@ -# Lucem 2.0.1 is here! -Yay. This is a hotfix release and you should update to it ASAP! - -**NOTICE**: Please run `lucem init` upon installing this release! +# Lucem 2.1.0 is here! +Yay. \ +This changelog contains every feature from 2.0.2 to 2.1.0 ## Fixed Bugs -* Editing the configuration with `lucem shell` would result in your configuration getting borked - -## Thank you to all these people :3 -* The Sober team for creating Sober (plox open source it so that I can rewrite it in Nim :3) -* adverbialsatz for reporting the bug - -## Installation -Run `nimble install https://github.com/xTrayambak/lucem` in your terminal. Remember, this requires a Nim toolchain with version 2.0 or higher. - -# Lucem 2.0.0 is here! -Yay. - -**NOTICE**: Please run `lucem init` upon installing this release! - -This release rewrites a huge part of Lucem by splitting it up into three components - the lucem CLI you all know (and love? hate? I don't know!) to be more modular. - -# What's Changed? -Lucem stands at 2.4K lines of code. - -## New Features -* Lucem now allows you to change the renderer with the `client:renderer` backend. Correct values for this are `opengl` or `vulkan`. -* Lucem now has an overlay on platforms that support it (KDE, Hyprland, Sway, Cosmic, or basically anything that isn't GNOME) -* Lucem now defaults to using Sober's RPC implementation as it is better. `lucem:discord_rpc` still works as intended. - -## Thank you to all of these people :3 -* The Sober team for creating Sober (plox open source it so that I can rewrite it in Nim :3) -* AshtakaOof for beta-testing the early rewrite builds +* The Flatpak command that'd install Sober would get stuck on a confirmation (2.0.2) +* `lucemd` no longer causes CPU spikes (2.0.3) +* `lucem_overlay` lets your compositor blur its surface (2.0.3) +* Added support for the new Sober configuration interface (2.0.3) +* Fixed botched symbolic icons in the settings shell (2.0.4) +* Fixed arbitrary daemon sleep time (2.0.4) +* Don't emit `--opengl` flag, use configuration instead (2.0.4) +* Lock FPS to 60 by default, preventing coil whine (2.1.0) + +## Additions +* Added autoupdater, this checks for updates every time Lucem is run. (2.1.0) +* You can now update Lucem by running `lucem update`. (2.1.0) +* Added update alert that shows up every time a new release is available. (2.1.0) +* Overhauled Lucem shell to make it nicer to use (2.1.0) +* Added new Lucem icon (2.0.4) ## Installation Run `nimble install https://github.com/xTrayambak/lucem` in your terminal. Remember, this requires a Nim toolchain with version 2.0 or higher. diff --git a/lucem.nimble b/lucem.nimble index 89b1ef3..d20e4eb 100644 --- a/lucem.nimble +++ b/lucem.nimble @@ -1,6 +1,6 @@ # Package -version = "2.0.4" +version = "2.1.0" author = "xTrayambak" description = "A small wrapper over Sober that provides quality of life improvements" license = "MIT" @@ -31,3 +31,5 @@ after install: echo "\e[1mThanks for installing Lucem!" echo "If you run `lucem` in the terminal and no command is found, try running the command below:\e[0m" echo "\e[1:32mexport PATH=\"$HOME/.nimble/bin:$PATH\"\e[0m" + +requires "semver >= 1.2.3" diff --git a/shell.nix b/shell.nix index a34e96e..f84798d 100755 --- a/shell.nix +++ b/shell.nix @@ -13,7 +13,7 @@ mkShell { xorg.libXext libxkbcommon libGL.dev - wayland + wayland.dev wayland-protocols wayland-scanner.dev ]; @@ -24,5 +24,6 @@ mkShell { pkg-config curl.dev openssl.dev + wayland.dev ]; } diff --git a/src/assets/lucem.png b/src/assets/lucem.png new file mode 100644 index 0000000..9540d20 Binary files /dev/null and b/src/assets/lucem.png differ diff --git a/src/commands/run.nim b/src/commands/run.nim index 6b26a3e..0e126d6 100644 --- a/src/commands/run.nim +++ b/src/commands/run.nim @@ -6,8 +6,7 @@ import ../api/[games, thumbnails, ipinfo] import ../patches/[bring_back_oof, patch_fonts, sun_and_moon_textures, windowing_backend] import ../shell/loading_screen -import ../proto -import ../sober_config +import ../[updater, sober_config, proto] import ../[ argparser, config, flatpak, common, meta, sugar, notifications, fflags, log_file, @@ -158,6 +157,7 @@ proc eventWatcher*( proc runRoblox*(input: Input, config: Config) = info "lucem: running Roblox via Sober" + runUpdateChecker(config) writeFile(getSoberLogPath(), newString(0)) diff --git a/src/config.nim b/src/config.nim index 2a0360e..f162612 100644 --- a/src/config.nim +++ b/src/config.nim @@ -32,6 +32,7 @@ type LucemConfig* = object discord_rpc*: bool = false + auto_updater*: bool = true notify_server_region*: bool = true loading_screen*: bool = true polling_delay*: uint = 100 diff --git a/src/http.nim b/src/http.nim index d41e8ae..78be1bd 100644 --- a/src/http.nim +++ b/src/http.nim @@ -1,6 +1,6 @@ ## Reducing code clutter when making HTTP requests import std/[logging, monotimes] -import pkg/[curly] +import pkg/[curly, webby] import ./meta {.passC: gorge("pkg-config --cflags libcurl").} @@ -10,10 +10,14 @@ var curl = newCurly() proc httpGet*(url: string): string = debug "http: making HTTP/GET request to " & url & "; allocating HttpClient" let + headers = toWebby(@[ + ("User-Agent", "lucem/" & Version) + ]) startReq = getMonoTime() - req = curl.get(url) + req = curl.get(url, headers) endReq = getMonoTime() debug "http: HTTP/GET request to " & url & " took " & $(endReq - startReq) + debug "http: response body:\n" & req.body req.body diff --git a/src/internal_fonts.nim b/src/internal_fonts.nim index 6258f1b..ae0e1c4 100644 --- a/src/internal_fonts.nim +++ b/src/internal_fonts.nim @@ -3,3 +3,4 @@ const IbmPlexSans* = staticRead("IBMPlexSans-Regular.ttf") LucemIcon* = staticRead("assets/lucem.svg") + LucemIconPng* = staticRead("assets/lucem.png") diff --git a/src/lucem.nim b/src/lucem.nim index 5d1c09a..6be7bec 100644 --- a/src/lucem.nim +++ b/src/lucem.nim @@ -4,7 +4,7 @@ import std/[os, logging, strutils, terminal] import colored_logger, nimgl/vulkan -import ./[meta, argparser, config, cache_calls, desktop_files, sober_state, gpu_info, systemd] +import ./[meta, argparser, config, cache_calls, desktop_files, sober_state, gpu_info, systemd, updater] import ./shell/core import ./commands/[init, run, edit_config, explain] @@ -17,6 +17,7 @@ Commands: run Run Sober meta Get build metadata list-gpus List all GPUs on this system + update Check for Lucem updates and install them edit-config Edit the configuration file clear-cache Clear the API caches that Lucem maintains shell Launch the Lucem configuration GUI @@ -95,6 +96,10 @@ proc main() {.inline.} = initializeSober(input) createLucemDesktopFile() installSystemdService() + of "update": + updateLucem() + of "check-for-updates": + runUpdateChecker(parseConfig(input)) of "install-systemd-service": installSystemdService() of "relaunch-daemon": diff --git a/src/lucem_overlay.nim b/src/lucem_overlay.nim index 91b4aa2..9620f33 100644 --- a/src/lucem_overlay.nim +++ b/src/lucem_overlay.nim @@ -11,15 +11,36 @@ privateAccess(WindowWaylandObj) privateAccess(WindowWayland) privateAccess(Window) +{.passC: gorge("pkg-config --cflags wayland-client").} +{.passL: gorge("pkg-config --libs wayland-client").} +{.passC: gorge("pkg-config --cflags x11").} +{.passL: gorge("pkg-config --libs x11").} +{.passC: gorge("pkg-config --cflags xcursor").} +{.passL: gorge("pkg-config --libs xcursor").} +{.passC: gorge("pkg-config --cflags xext").} +{.passL: gorge("pkg-config --libs xext").} +{.passC: gorge("pkg-config --cflags xkbcommon").} +{.passL: gorge("pkg-config --libs xkbcommon").} +{.passC: gorge("pkg-config --cflags gl").} +{.passL: gorge("pkg-config --libs gl").} + type + OverlayState* = enum + osOverlay + osUpdateAlert + Overlay* = object heading*: string description*: string expireTime*: float + state*: OverlayState = osOverlay + icon*: Option[string] closed*: bool config*: Config + lucemImage*: Image + vg*: NVGContext wl*: WindowWaylandOpengl size*: IVec2 = ivec2(600, 200) @@ -47,13 +68,26 @@ proc draw*(overlay: var Overlay) = overlay.vg.textAlign(haLeft, vaTop) overlay.vg.fontSize(overlay.config.overlay.headingSize) overlay.vg.fillColor(white(255)) - discard overlay.vg.text(16f, 16f, overlay.heading) + + var icon = cast[seq[byte]](LucemIconPng) + overlay.lucemImage = overlay.vg.createImageMem(data = icon) + + if overlay.state == osOverlay: + discard overlay.vg.text(16f, 16f, overlay.heading) + else: + let imgPaint = overlay.vg.imagePattern(16, 16, 60, 60, 0, overlay.lucemImage, 1f) + overlay.vg.beginPath() + overlay.vg.rect(16, 16, 60, 60) + overlay.vg.fillPaint(imgPaint) + overlay.vg.fill() + + discard overlay.vg.text(100f, 16f, overlay.heading) overlay.vg.fontFace("heading") overlay.vg.textAlign(haLeft, vaTop) overlay.vg.fontSize(overlay.config.overlay.descriptionSize) overlay.vg.fillColor(white(255)) - discard overlay.vg.text(16f, 64f, overlay.description) + overlay.vg.textBox(16f, 100f, 512f, overlay.description, nil) # TODO: icon rendering, even though we don't use them yet # but it'd be useful for the future @@ -62,15 +96,29 @@ proc draw*(overlay: var Overlay) = proc initOverlay*(input: Input) {.noReturn.} = var overlay: Overlay - for opt in [ + + let opts = if not input.enabled("update-alert"): + @[ "heading", "description", "expire-time" - ]: + ] + else: + @[ + "update-heading", + "update-message" + ] + + overlay.state = if input.enabled("update-alert"): + osUpdateAlert + else: + osOverlay + + for opt in opts: if (let maybeOpt = input.flag(opt); *maybeOpt): case opt - of "heading": overlay.heading = decode(&maybeOpt) - of "description": overlay.description = decode(&maybeOpt) + of "heading", "update-heading": overlay.heading = decode(&maybeOpt) + of "description", "update-message": overlay.description = decode(&maybeOpt) of "expire-time": overlay.expireTime = parseFloat(&maybeOpt) else: error "overlay: expected flag: " & opt @@ -92,15 +140,19 @@ proc initOverlay*(input: Input) {.noReturn.} = ) overlay.wl.setKeyboardInteractivity(LayerInteractivityMode.None) var anchors: seq[LayerEdge] - for value in config.overlay.anchors.split('-'): - debug "overlay: got anchor: " & value - case value.toLowerAscii() - of "left", "l": anchors &= LayerEdge.Left - of "right", "r": anchors &= LayerEdge.Right - of "top", "up", "u": anchors &= LayerEdge.Top - of "bottom", "down", "d": anchors &= LayerEdge.Bottom - else: - warn "overlay: unhandled anchor: " & value + + if overlay.state == osOverlay: + for value in config.overlay.anchors.split('-'): + debug "overlay: got anchor: " & value + case value.toLowerAscii() + of "left", "l": anchors &= LayerEdge.Left + of "right", "r": anchors &= LayerEdge.Right + of "top", "up", "u": anchors &= LayerEdge.Top + of "bottom", "down", "d": anchors &= LayerEdge.Bottom + else: + warn "overlay: unhandled anchor: " & value + else: + anchors = @[LayerEdge.Left, LayerEdge.Right, LayerEdge.Top, LayerEdge.Bottom] overlay.wl.setAnchor(anchors) overlay.wl.setExclusiveZone(10000) @@ -133,6 +185,9 @@ proc initOverlay*(input: Input) {.noReturn.} = overlay.draw() overlay.wl.eventsHandler.onTick = proc(event: TickEvent) = + if overlay.expireTime == 0f: + return # Infinite alert + let epoch = epochTime() let elapsed = epoch - overlay.lastEpoch diff --git a/src/notifications.nim b/src/notifications.nim index ab34d6e..1a9ccff 100644 --- a/src/notifications.nim +++ b/src/notifications.nim @@ -8,7 +8,7 @@ proc notifyFallback*( expireTime: uint64 = 240000, icon: Option[string] = none(string), ) = - debug "notifications: using libnotify fallback... (cringe guhnome user detected)" + debug "notifications: using libnotify fallback (cringe guhnome user detected)" debug "notifications: preparing notify-send command" debug "notifications: heading = $1, description = $2, expireTime = $3" % [heading, description, $expireTime] @@ -59,3 +59,24 @@ proc notify*( debug "notifications: executing command: " & cmd discard execCmd(cmd) quit(0) + +proc presentUpdateAlert*( + heading: string, + message: string +) = + var worker = findExe("lucem_overlay") + if getEnv("XDG_CURRENT_DESKTOP") == "GNOME" or (defined(release) and worker.len < 1): + warn "notifications: we're either on GNOME or the lucem overlay binary is missing, something went horribly wrong!" + notifyFallback(heading, message, 240000) + return + + let pid = fork() + + if worker.len < 1 and not defined(release): + worker = "./lucem_overlay" + + if pid == 0: + let cmd = worker & " --update-alert --update-heading:\"" & heading.encode() & "\" --update-message:\"" & message.encode() & '"' + debug "notifications: executing command: " & cmd + discard execCmd(cmd) + quit(0) diff --git a/src/updater.nim b/src/updater.nim new file mode 100644 index 0000000..50f5be9 --- /dev/null +++ b/src/updater.nim @@ -0,0 +1,144 @@ +## Lucem auto-updater +## Copyright (C) 2024 Trayambak Rai +import std/[os, osproc, logging, tempfiles, distros, posix] +import pkg/[semver, jsony] +import ./[http, argparser, config, sugar, meta, notifications] + +type + ReleaseAuthor* = object + login*: string + id*: uint32 + node_id*, avatar_url*, gravatar_id*, url*, html_url*, followers_url*, following_url*, gists_url*, starred_url*, subscriptions_url*, organizations_url*, repos_url*, events_url*, received_events_url*, `type`*, user_view_type*: string + site_admin*: bool + + LucemRelease* = object + url*, assets_url*, upload_url*, html_url*: string + id*: uint64 + author*: ReleaseAuthor + node_id*, tag_name*, target_commitish*, name*: string + draft*, prerelease*: bool + created_at*, published_at*: string + assets*: seq[string] + tarball_url*, zipball_url*: string + +const + LucemReleaseUrl {.strdefine.} = "https://api.github.com/repos/xTrayambak/lucem/releases/latest" + +proc getLatestRelease*(): Option[LucemRelease] {.inline.} = + debug "lucem: auto-updater: fetching latest release" + try: + return httpGet( + LucemReleaseUrl + ).fromJson( + LucemRelease + ).some() + except JsonError as exc: + warn "lucem: auto-updater: cannot parse release data: " & exc.msg + except CatchableError as exc: + warn "lucem: auto-updater: cannot get latest release: " & exc.msg & " (" & $exc.name & ')' + +proc runUpdateChecker*(config: Config) = + if not config.lucem.autoUpdater: + debug "lucem: auto-updater: skipping update checks as auto-updater is disabled in config" + return + + when defined(lucemDisableAutoUpdater): + debug "lucem: auto-updater: skipping update checks as auto-updater is disabled by a compile-time flag (--define:lucemDisableAutoUpdater)" + return + + debug "lucem: auto-updater: running update checks" + let release = getLatestRelease() + + if !release: + warn "lucem: auto-updater: cannot get release, skipping checks." + return + + let data = &release + let newVersion = try: + parseVersion(data.tagName).some() + except semver.ParseError as exc: + warn "lucem: auto-updater: cannot parse new semver: " & exc.msg & " (" & data.tagName & ')' + none(Version) + + if !newVersion: + return + + let currVersion = parseVersion(meta.Version) + + debug "lucem: auto-updater: new version: " & $(&newVersion) + debug "lucem: auto-updater: current version: " & $currVersion + + let newVer = &newVersion + + if newVer > currVersion: + info "lucem: found a new release! (" & $newVer & ')' + presentUpdateAlert( + "Lucem " & $newVer & " is out!", + "A new version of Lucem is out. You are strongly advised to update to this release for bug fixes and other improvements." + ) + elif newVer == currVersion: + debug "lucem: user is on the latest version of lucem" + elif newVer < currVersion: + warn "lucem: version mismatch (newest release: " & $newVer & ", version this binary was tagged as: " & $currVersion & ')' + warn "lucem: are you using a development version? :P" + +proc updateLucem* = + info "lucem: checking for updates" + let release = getLatestRelease() + + if !release: + error "lucem: cannot get current release" + return + + let currVersion = parseVersion(meta.Version) + let newVer = parseVersion((&release).tagName) + + if newVer != currVersion: + info "lucem: found new version! (" & $newVer & ')' + let wd = getCurrentDir() + let tmpDir = createTempDir("lucem-", '-' & $newVer) + + let git = findExe("git") + let nimble = findExe("nimble") + + if nimble.len < 1: + error "lucem: cannot find `nimble`!" + quit(1) + + if git.len < 1: + error "lucem: cannot find `git`!" + quit(1) + + info "lucem: cloning source code" + if (let code = execCmd(git & " clone https://github.com/xTrayambak/lucem.git " & tmpDir); code != 0): + error "lucem: git exited with non-zero exit code: " & $code + quit(1) + + discard chdir(tmpDir.cstring) + + info "lucem: switching to " & $newVer & " branch" + if (let code = execCmd(git & " checkout " & $newVer); code != 0): + error "lucem: git exited with non-zero exit code: " & $code + quit(1) + + info "lucem: compiling lucem" + if not detectOs(NixOS): + if (let code = execCmd(nimble & " install"); code != 0): + error "lucem: nimble exited with non-zero exit code: " & $code + quit(1) + else: + info "lucem: Nix environment detected, entering Nix shell" + let nix = findExe("nix-shell") & "-shell" # FIXME: for some reason, `nix-shell` returns the `nix` binary instead here. Perhaps a Nim STL bug + if nix.len < 1: + error "lucem: cannot find `nix-shell`!" + quit(1) + + if (let code = execCmd(nix & " --run \"" & nimble & " install\""); code != 0): + error "lucem: nix-shell or nimble exited with non-zero exit code: " & $code + quit(1) + + info "lucem: updated successfully!" + info "Lucem is now at version " & $newVer + else: + info "lucem: nothing to do." + quit(0)