Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 243a41fa7a | |||
| 7cce11dc66 | |||
| fc808a6c28 | |||
| c6b3f65b35 | |||
| 427a872811 | |||
| 1e5527f629 | |||
| 6d5404337e | |||
| 8c06248c32 | |||
| 7d5b41a7f7 | |||
| 302136c28b | |||
| 4b4e1968b3 | |||
| 8272352d64 | |||
| f2ee812bc5 | |||
| edf33e2afd | |||
| 9d9285df23 | |||
| c46f5ee492 | |||
| 4d82414fdd | |||
| 2aa73f581f | |||
| 3a29ffad57 | |||
| 7fd48da51e | |||
| 05c7bb3291 | |||
| 13cf05bf99 | |||
| 06901c72c0 | |||
| 037fdc2778 | |||
| 8819b3fa0d | |||
| 22ceac1a9b | |||
| d504f8cd5d | |||
| 89ccd5ff02 | |||
| bc728f479d | |||
| 1b82aacb7a | |||
| c43bb4c690 | |||
| e7df1883aa | |||
| 4a3b9fbcd5 | |||
| 41ea38df2e | |||
| 1a3c87bb2d | |||
| 4683e3c5c5 | |||
| 87bfce1a46 | |||
| 2907cf6574 | |||
| 6c81d89d41 | |||
| a11f1517ff | |||
| ecca9e6aa6 | |||
| a2ba7f1a37 | |||
| 2d7a2be529 | |||
| da67899921 | |||
| 11fcf0cfc2 | |||
| 2a5c7ba463 | |||
| 8db6b5afaa | |||
| 54501c9390 | |||
| 4c1335378d | |||
| 31014f4f14 | |||
| 2ea3eb053e | |||
| e7a5934e9a | |||
| 9e63f77e61 | |||
| 7e07e38ab2 | |||
| 37cb7d9299 | |||
| 55bc9f3211 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1 +1,4 @@
|
||||
bin/
|
||||
### Above combination will ignore all files without extension ###
|
||||
bin/
|
||||
.vscode/
|
||||
pixctrl.js
|
||||
49
README.md
Normal file
49
README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# randopix
|
||||
## Usage
|
||||
### Server
|
||||
The server is run with an inital mode. All settings can later be changed with the client.
|
||||
```
|
||||
randopix
|
||||
|
||||
Version 0.1 - Display random images from different sources
|
||||
|
||||
Usage:
|
||||
randopix [options]
|
||||
|
||||
Options:
|
||||
-m, --mode=MODE The image source mode. Possible values: [none, foxes, inspiro, file]
|
||||
-p, --path=PATH Path to a directory with images for the 'file' mode
|
||||
-t, --timeout=TIMEOUT Seconds before the image is refreshed (default: 300)
|
||||
-w, --windowed Do not start in fullscreen mode
|
||||
-v, --verbose Show more information
|
||||
-h, --help Show this help
|
||||
```
|
||||
### Client
|
||||
The `pixctrl` client is used to issue commands to a running server.
|
||||
Per default the client will try to connect to a server running on the same maschine. Use the `-s HOSTNAME` option to control a server over the network.
|
||||
```
|
||||
pixctrl
|
||||
|
||||
Control utilitiy for randopix
|
||||
|
||||
Usage:
|
||||
pixctrl [options] COMMAND
|
||||
|
||||
Commands:
|
||||
|
||||
refresh Force image refresh now
|
||||
timeout Set timeout in seconds before a new image is displayed
|
||||
mode Change the display mode. Possible values: [none, foxes, inspiro, file]
|
||||
|
||||
Options:
|
||||
-s, --server=SERVER Host running the randopix server (default: 127.0.0.1)
|
||||
-p, --port=PORT Port to connect to the randopix server (default: 5555)
|
||||
-h, --help Show this help
|
||||
```
|
||||
## Build
|
||||
Install the [Nim Compiler](https://nim-lang.org/install.html).
|
||||
|
||||
Use this command to install the dependencies and build the program:
|
||||
```
|
||||
$ nimble build
|
||||
```
|
||||
9
examples/randopix.service
Normal file
9
examples/randopix.service
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=randopix image display
|
||||
|
||||
[Service]
|
||||
Environment=DISPLAY=:0
|
||||
ExecStart=randopix -d /mnt/pix -m file
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -1,18 +0,0 @@
|
||||
import strformat
|
||||
# Package
|
||||
|
||||
version = "0.1.0"
|
||||
author = "luxick"
|
||||
description = "Play an image slide show from different sources"
|
||||
license = "GPL-2.0"
|
||||
srcDir = "src"
|
||||
bin = @["randopics"]
|
||||
|
||||
# Dependencies
|
||||
requires "nim >= 1.0.0", "gintro <= 0.5.5"
|
||||
|
||||
task debug, "Compile debug version":
|
||||
exec "nim c --out:bin/randopics src/randopics.nim"
|
||||
|
||||
task release, "Compile release version":
|
||||
exec fmt"nim c -d:release --out:bin/{version}/randopics src/randopics.nim"
|
||||
31
randopix.nimble
Normal file
31
randopix.nimble
Normal file
@@ -0,0 +1,31 @@
|
||||
# Package
|
||||
|
||||
version = "1.1.0"
|
||||
author = "luxick"
|
||||
description = "Play an image slide show from different sources"
|
||||
license = "GPL-2.0"
|
||||
srcDir = "src"
|
||||
binDir = "bin"
|
||||
bin = @["randopix", "pixctrl"]
|
||||
|
||||
# Dependencies
|
||||
requires "nim >= 1.0.0", "gintro", "argparse", "jester", "ajax"
|
||||
# Not on nimble yet
|
||||
requires "https://github.com/luxick/op.git >= 1.0.0"
|
||||
|
||||
proc genJS =
|
||||
echo "Generating JS Client"
|
||||
exec("nim js -o:src/resources/www/pixctrl.js src/pixctrl.nim")
|
||||
|
||||
task genJS, "Generate the Javascript client":
|
||||
genJS()
|
||||
|
||||
task buildAll, "Generate JS and run build":
|
||||
genJS()
|
||||
exec "nimble build"
|
||||
|
||||
task debug, "Compile debug version":
|
||||
exec "nim c -d:debug --debugger:native -o:bin/randopix src/randopix.nim"
|
||||
|
||||
before install:
|
||||
genJS()
|
||||
38
src/common.nim
Normal file
38
src/common.nim
Normal file
@@ -0,0 +1,38 @@
|
||||
import json
|
||||
|
||||
type
|
||||
OpResult* = object of RootObj ## Result object for signalling failure state across proc calls
|
||||
success*: bool ## Indicating if the opration was successfull
|
||||
errorMsg*: string ## Error meassge in case the operation failed
|
||||
|
||||
Mode* {.pure.} = enum ## Options for the display mode
|
||||
None = "none" ## No images will be displayed
|
||||
Foxes = "foxes" ## Some nice foxes
|
||||
Inspiro = "inspiro" ## Inspiring nonsense
|
||||
File = "file" ## Images from a local path
|
||||
|
||||
Command* = enum
|
||||
cClose = "close" ## Closes the control server and exists the applicaiton
|
||||
cRefresh = "refresh" ## Force refresh of the image now
|
||||
cTimeout = "timeout" ## Set image timeout to a new value
|
||||
cMode = "mode" ## Change the servers display mode
|
||||
|
||||
CommandMessage* = object of RootObj
|
||||
command*: Command ## Command that the application should execute
|
||||
parameter*: string ## Optional parameter for the command
|
||||
|
||||
proc `$`(cMsg: CommandMessage): string =
|
||||
$(%cMsg)
|
||||
|
||||
proc newOpResult*(): OpResult =
|
||||
OpResult(success: true)
|
||||
|
||||
proc newOpResult*(msg: string): OpResult =
|
||||
OpResult(success: false, errorMsg: msg)
|
||||
|
||||
proc newCommandMessage*(c: Command, p: string = ""): CommandMessage =
|
||||
CommandMessage(command: c, parameter: p)
|
||||
|
||||
proc enumToStrings*(en: typedesc): seq[string] =
|
||||
for x in en:
|
||||
result.add $x
|
||||
94
src/pixctrl.nim
Normal file
94
src/pixctrl.nim
Normal file
@@ -0,0 +1,94 @@
|
||||
import strutils, json
|
||||
import common
|
||||
|
||||
when defined(js):
|
||||
import ajax, jsconsole, dom
|
||||
else:
|
||||
import httpClient, strformat
|
||||
import argparse
|
||||
var randopixServer* {.exportc.}: string ## URL for the randopix server
|
||||
|
||||
proc sendCommand(msg: CommandMessage) =
|
||||
when defined(js):
|
||||
console.log("Sending:", $msg, "to URL:", document.URL)
|
||||
var req = newXMLHttpRequest()
|
||||
|
||||
proc processSend(e:Event) =
|
||||
if req.readyState == rsDONE:
|
||||
if req.status != 200:
|
||||
console.log("There was a problem with the request.")
|
||||
console.log($req.status, req.statusText)
|
||||
|
||||
req.onreadystatechange = processSend
|
||||
req.open("POST", document.URL)
|
||||
req.send(cstring($(%*msg)))
|
||||
else:
|
||||
let client = newHttpClient()
|
||||
let resp = client.post(randopixServer, $(%msg))
|
||||
if not resp.status.contains("200"):
|
||||
echo "Error while sending command: ", resp.status
|
||||
|
||||
proc sendCommand(cmd: Command) =
|
||||
sendCommand(newCommandMessage(cmd))
|
||||
|
||||
proc switchMode*(mode: string) =
|
||||
## Update the display mode
|
||||
try:
|
||||
discard parseEnum[Mode](mode)
|
||||
except ValueError:
|
||||
echo "Invalid mode: ", mode
|
||||
echo "Possible values: [$1]" % Mode.enumToStrings().join(", ")
|
||||
return
|
||||
sendCommand(newCommandMessage(cMode, mode))
|
||||
|
||||
proc refresh*() {.exportc.} =
|
||||
## Force refresh of the current image
|
||||
sendCommand(cRefresh)
|
||||
|
||||
proc setTimeout*(seconds: string) =
|
||||
## Set the image timeout to this value
|
||||
sendCommand(newCommandMessage(cTimeout, seconds))
|
||||
|
||||
when defined(js):
|
||||
proc getModes(): seq[cstring] {.exportc.} =
|
||||
for mode in enumToStrings(Mode):
|
||||
result.add cstring(mode)
|
||||
proc switchMode*(mode: cstring) {.exportc.} =
|
||||
switchMode($mode)
|
||||
proc setTimeout*(seconds: cstring) {.exportc.} =
|
||||
setTimeout($seconds)
|
||||
else:
|
||||
when isMainModule:
|
||||
const modeHelp = "Change the display mode. Possible values: [$1]" % Mode.enumToStrings().join(", ")
|
||||
var p = newParser("pixctrl"):
|
||||
help("Control utilitiy for randopix")
|
||||
option("-s", "--server", help="Host running the randopix server", default="http://localhost:8080/")
|
||||
run:
|
||||
if opts.server.startsWith("http://"):
|
||||
randopixServer = opts.server
|
||||
else:
|
||||
randopixServer = fmt"http://{opts.server}"
|
||||
|
||||
command($cRefresh):
|
||||
## Force refresh command
|
||||
help("Force image refresh now")
|
||||
run:
|
||||
refresh()
|
||||
|
||||
command($cTimeout):
|
||||
## Timeout Command
|
||||
help("Set timeout in seconds before a new image is displayed")
|
||||
arg("seconds", default = "300")
|
||||
run:
|
||||
setTimeout(opts.seconds)
|
||||
|
||||
command($cMode):
|
||||
## Mode switch command
|
||||
help(modeHelp)
|
||||
arg("mode")
|
||||
run:
|
||||
switchMode(opts.mode)
|
||||
try:
|
||||
p.run(commandLineParams())
|
||||
except:
|
||||
echo getCurrentExceptionMsg()
|
||||
174
src/providers.nim
Normal file
174
src/providers.nim
Normal file
@@ -0,0 +1,174 @@
|
||||
import os, sets, random, httpClient, json, strutils, strformat, options, deques, times
|
||||
from lenientops import `*`
|
||||
import op, gintro/[gdkpixbuf, gobject]
|
||||
import common
|
||||
|
||||
const
|
||||
supportedExts = @[".png", ".jpg", ".jpeg"]
|
||||
placeholderImg = slurp("resources/blank.png")
|
||||
foxesUrl = "https://randomfox.ca/floof/"
|
||||
inspiroUrl = "http://inspirobot.me/api?generate=true"
|
||||
|
||||
type
|
||||
ImageProvider* = ref object of RootObj
|
||||
## Manages images that should be displayed
|
||||
verbose: bool ## Additional logging for the image provider
|
||||
mode* : Mode ## Selects the API that is used to get images
|
||||
path*: Option[string] ## Path on the local file syetem that will be used in `file` mode
|
||||
exts: HashSet[string] ## Allowed extensions that the `file` mode will display
|
||||
|
||||
var
|
||||
client = newHttpClient() ## For loading images from the web
|
||||
tmpDir = getTempDir() / "randopix"
|
||||
tmpFile = tmpDir / "tmp.png"
|
||||
fileList = initDeque[string]()
|
||||
|
||||
########################
|
||||
# Constructors
|
||||
########################
|
||||
|
||||
proc newImageProvider(verbose: bool, mode: Mode, path: Option[string]): ImageProvider =
|
||||
ImageProvider(verbose: verbose, mode: mode, path: path, exts: supportedExts.toHashSet)
|
||||
|
||||
proc newImageProvider*(verbose: bool): ImageProvider =
|
||||
newImageProvider(verbose, Mode.None, none(string))
|
||||
|
||||
proc newImageProvider*(verbose: bool, path: string): ImageProvider =
|
||||
newImageProvider(verbose, Mode.None, some(path))
|
||||
|
||||
proc newImageProvider*(verbose: bool, mode: Mode): ImageProvider =
|
||||
newImageProvider(verbose, mode, none(string))
|
||||
|
||||
proc newImageProvider*(verbose: bool, mode: Mode, path: string): ImageProvider =
|
||||
newImageProvider(verbose, mode, some(path))
|
||||
|
||||
########################
|
||||
# Utilities
|
||||
########################
|
||||
|
||||
proc log(ip: ImageProvider, things: varargs[string, `$`]) =
|
||||
if ip.verbose:
|
||||
echo things.join()
|
||||
|
||||
func calcImageSize(maxWidth, maxHeight, imgWidth, imgHeight: int): tuple[width: int, height: int] =
|
||||
## Calculate the best fit for an image on the give screen size.
|
||||
## This should keep the image aspect ratio
|
||||
let
|
||||
ratioMax = maxWidth / maxHeight
|
||||
ratioImg = imgWidth / imgHeight
|
||||
if (ratioMax > ratioImg):
|
||||
result.width = (imgWidth * (maxHeight / imgHeight)).toInt
|
||||
result.height = maxHeight
|
||||
else:
|
||||
result.width = maxWidth
|
||||
result.height = (imgHeight * (maxWidth / imgWidth)).toInt
|
||||
|
||||
########################
|
||||
# Image Provider procs
|
||||
########################
|
||||
|
||||
proc getPlaceHolder(ip: ImageProvider): OP[string] =
|
||||
## Provide the placeholder image.
|
||||
## This is used when no mode is active
|
||||
let f = fmt"{tmpFile}.blank"
|
||||
writeFile(f, placeholderImg)
|
||||
ok f
|
||||
|
||||
proc getFox(ip: ImageProvider): OP[string] =
|
||||
## Download image from the fox API
|
||||
try:
|
||||
let urlData = client.getContent(foxesUrl)
|
||||
let info = parseJson(urlData)
|
||||
let imageData = client.getContent(info["image"].getStr)
|
||||
let dlFile = fmt"{tmpFile}.download"
|
||||
writeFile(dlFile, imageData)
|
||||
ok dlFile
|
||||
except JsonParsingError:
|
||||
ip.log fmt"Error while fetching from fox API: {getCurrentExceptionMsg()}"
|
||||
fail[string] "Json parsing error"
|
||||
except KeyError:
|
||||
ip.log fmt"No image in downloaded data: {getCurrentExceptionMsg()}"
|
||||
fail[string] "No image from API"
|
||||
|
||||
proc getInspiro(ip: ImageProvider): OP[string] =
|
||||
## Download and save image from the inspiro API
|
||||
try:
|
||||
let imageUrl = client.getContent(inspiroUrl)
|
||||
ip.log fmt"Downloading inspiro image from: '{imageUrl}'"
|
||||
let imageData = client.getContent(imageUrl)
|
||||
let dlFile = fmt"{tmpFile}.download"
|
||||
writeFile(dlFile,imageData)
|
||||
ok dlFile
|
||||
except:
|
||||
ip.log fmt"Unexpected error while downloading: {getCurrentExceptionMsg()}"
|
||||
fail[string] getCurrentExceptionMsg()
|
||||
|
||||
proc getLocalFile(ip: var ImageProvider): OP[string] =
|
||||
## Provide an image from a local folder
|
||||
|
||||
# First, check if there are still images left to be loaded.
|
||||
# If not reread all files from the path
|
||||
if fileList.len == 0:
|
||||
var tmp: seq[string]
|
||||
var split: tuple[dir, name, ext: string]
|
||||
for file in walkDirRec(ip.path.get):
|
||||
split = splitFile(file)
|
||||
if ip.exts.contains(split.ext):
|
||||
tmp.add($file)
|
||||
|
||||
ip.log fmt"Loaded {tmp.len} files"
|
||||
shuffle(tmp)
|
||||
for file in tmp:
|
||||
fileList.addLast(file)
|
||||
if fileList.len == 0:
|
||||
return fail[string] "No files found"
|
||||
|
||||
let next = fileList.popFirst()
|
||||
# Remove the current file after
|
||||
ok next
|
||||
|
||||
proc getFileName(ip: var ImageProvider): OP[string] =
|
||||
## Get the temporary file name of the next file to display
|
||||
case ip.mode
|
||||
of Mode.None:
|
||||
return ip.getPlaceHolder()
|
||||
of Mode.File:
|
||||
return ip.getLocalFile()
|
||||
of Mode.Foxes:
|
||||
return ip.getFox()
|
||||
of Mode.Inspiro:
|
||||
return ip.getInspiro()
|
||||
|
||||
########################
|
||||
# Exported procs
|
||||
########################
|
||||
|
||||
proc next*(ip: var ImageProvider, maxWidth, maxHeight: int): OP[string] =
|
||||
## Uses the image provider to get a new image ready to display.
|
||||
## `width` and `height` should be the size of the window.
|
||||
|
||||
let r = ip.getFileName()
|
||||
if not r.isOk: return r
|
||||
|
||||
var rawPixbuf = newPixbufFromFile(r.val)
|
||||
# Resize the pixbuf to best fit on screen
|
||||
let size = calcImageSize(maxWidth, maxHeight, rawPixbuf.width, rawPixbuf.height)
|
||||
ip.log "Scale image to: ", size
|
||||
let then = now()
|
||||
var pixbuf = rawPixbuf.scaleSimple(size.width, size.height, InterpType.nearest)
|
||||
let now = now()
|
||||
ip.log "Image scaled. Time: ", (now - then).inMilliseconds, "ms"
|
||||
# The pixbuf is written to disk and loaded again once because
|
||||
# directly setting the image from a pixbuf will leak memory
|
||||
let saved = pixbuf.savev(tmpFile, "png", @[])
|
||||
if not saved:
|
||||
return result.fail "Error while saving temporary image"
|
||||
|
||||
# GTK pixbuf leaks memory when not manually decreasing reference count
|
||||
pixbuf.unref()
|
||||
rawPixbuf.unref()
|
||||
|
||||
ok tmpFile
|
||||
|
||||
createDir(tmpDir)
|
||||
randomize()
|
||||
@@ -1,5 +0,0 @@
|
||||
import ../randopics
|
||||
import gintro/[gdkpixbuf]
|
||||
|
||||
proc get(): Pixbuf =
|
||||
discard
|
||||
@@ -1,104 +0,0 @@
|
||||
import os, httpClient, json, threadpool
|
||||
import gintro/[gtk, glib, gobject, gio, gdkpixbuf]
|
||||
|
||||
var
|
||||
window: ApplicationWindow
|
||||
fullscreen = true
|
||||
|
||||
const
|
||||
floofUrl = "https://randomfox.ca/floof/"
|
||||
updateTime = 300
|
||||
|
||||
type ImageProvider =
|
||||
tuple[
|
||||
get: proc(): Pixbuf
|
||||
]
|
||||
|
||||
proc downloadImage(): Pixbuf =
|
||||
let client = newHttpClient()
|
||||
let urlData = client.getContent(floofUrl)
|
||||
let info = parseJson(urlData)
|
||||
let imageData = client.getContent(info["image"].getStr)
|
||||
let loader = newPixbufLoader()
|
||||
discard loader.write(imageData)
|
||||
loader.getPixbuf()
|
||||
|
||||
proc resizeImage(pixbuf: Pixbuf, maxWidth, maxHeight: int): Pixbuf =
|
||||
var width, height: int
|
||||
if (maxWidth > maxHeight):
|
||||
height = maxHeight
|
||||
width = ((pixbuf.width * height) / pixbuf.height).toInt
|
||||
else:
|
||||
width = maxWidth
|
||||
height = ((pixbuf.height * width) / pixbuf.width).toInt
|
||||
|
||||
pixbuf.scaleSimple(width, height, InterpType.bilinear)
|
||||
|
||||
proc replaceImage(widget: Image, width, height: int) =
|
||||
var pixbuf = downloadImage()
|
||||
pixbuf = pixbuf.resizeImage(width, height)
|
||||
widget.setFromPixbuf(pixbuf)
|
||||
|
||||
proc updateCommand(action: SimpleAction; parameter: Variant; widget: Image) =
|
||||
var width, height: int
|
||||
window.getSize(width, height)
|
||||
replaceImage(widget, width, height)
|
||||
|
||||
proc toggleFullscreen(action: SimpleAction; parameter: Variant; window: ApplicationWindow) =
|
||||
if fullscreen:
|
||||
window.unfullscreen
|
||||
else:
|
||||
window.fullscreen
|
||||
fullscreen = not fullscreen
|
||||
|
||||
proc quit(action: SimpleAction; parameter: Variant; app: Application) =
|
||||
app.quit()
|
||||
|
||||
proc runUpdater(window: ApplicationWindow, image: Image) =
|
||||
echo "Start Updater"
|
||||
var width, height: int
|
||||
window.getSize(width, height)
|
||||
spawn replaceImage(image, width, height)
|
||||
|
||||
proc appActivate(app: Application) =
|
||||
window = newApplicationWindow(app)
|
||||
window.title = "Randopics"
|
||||
window.setKeepAbove(true)
|
||||
|
||||
let cssProvider = newCssProvider()
|
||||
let data = "window { background: black; }"
|
||||
discard cssProvider.loadFromData(data)
|
||||
let styleContext = window.getStyleContext()
|
||||
styleContext.addProvider(cssProvider, STYLE_PROVIDER_PRIORITY_USER)
|
||||
|
||||
let imageWidget = newImage()
|
||||
window.add(imageWidget)
|
||||
window.connect("show", runUpdater, imageWidget)
|
||||
|
||||
if fullscreen:
|
||||
window.fullscreen
|
||||
|
||||
let fullscreenAction = newSimpleAction("fullscreen")
|
||||
discard fullscreenAction.connect("activate", toggleFullscreen, window)
|
||||
app.setAccelsForAction("win.fullscreen", "F")
|
||||
window.actionMap.addAction(fullscreenAction)
|
||||
|
||||
let quitAction = newSimpleAction("quit")
|
||||
discard quitAction.connect("activate", quit, app)
|
||||
app.setAccelsForAction("win.quit", "Escape")
|
||||
window.actionMap.addAction(quitAction)
|
||||
|
||||
let updateAction = newSimpleAction("update")
|
||||
discard updateAction.connect("activate", updateCommand, imageWidget)
|
||||
app.setAccelsForAction("win.update", "U")
|
||||
window.actionMap.addAction(updateAction)
|
||||
|
||||
window.showAll
|
||||
|
||||
proc main =
|
||||
let app = newApplication("org.gtk.example")
|
||||
connect(app, "activate", appActivate)
|
||||
discard run(app)
|
||||
|
||||
when isMainModule:
|
||||
main()
|
||||
@@ -1,2 +0,0 @@
|
||||
threads:on
|
||||
d:ssl
|
||||
292
src/randopix.nim
Normal file
292
src/randopix.nim
Normal file
@@ -0,0 +1,292 @@
|
||||
import os, options, strformat
|
||||
import op
|
||||
import gintro/[glib, gobject, gtk, gio]
|
||||
import gintro/gdk except Window
|
||||
import argparse except run
|
||||
import providers, server, common
|
||||
|
||||
const
|
||||
css = slurp("resources/app.css")
|
||||
version = slurp("version")
|
||||
helpString = [
|
||||
"ESC\tClose program",
|
||||
"H\tShow/Hide this help",
|
||||
"F\tToggle fullscreen",
|
||||
"U\tForce refresh"
|
||||
].join("\n")
|
||||
|
||||
type
|
||||
Args = ref object
|
||||
fullscreen: bool ## Applicaion is show in fullscreen mode
|
||||
verbose: bool ## More debug information in notification label
|
||||
timeout: int ## Milliseconds between image refreshes
|
||||
port: int ## Port to host the control server
|
||||
|
||||
var
|
||||
imageProvider: ImageProvider ## Gets images from the chosen source
|
||||
args: Args ## The parsed command line args
|
||||
updateTimeout, serverTimeout: int ## ID of the timeouts for image updating and server checking
|
||||
# Widgets
|
||||
window: ApplicationWindow
|
||||
label: Label
|
||||
box: Box
|
||||
# Server vor recieving commands from external tools
|
||||
serverWorker: system.Thread[ServerArgs]
|
||||
|
||||
proc log(things: varargs[string, `$`]) =
|
||||
if args.verbose:
|
||||
echo things.join()
|
||||
|
||||
proc notify(label: Label, things: varargs[string, `$`]) =
|
||||
## Shows the notification box in the lower left corner.
|
||||
## If no message is passed, the box will be hidden
|
||||
label.text = things.join()
|
||||
if (label.text == ""):
|
||||
box.hide
|
||||
else:
|
||||
box.show
|
||||
|
||||
proc newArgs(): Option[Args] =
|
||||
let p = newParser("randopix"):
|
||||
help(fmt"Version {version} - Display random images from different sources")
|
||||
option("-m", "--mode", help="The image source mode.", choices=enumToStrings(Mode))
|
||||
option("-d", "--directoy", help="Path to a directory with images for the 'file' mode")
|
||||
option("-t", "--timeout", help="Seconds before the image is refreshed", default="300")
|
||||
option("-p", "--port", help="Port over which the control server should be accessible", default="8080")
|
||||
flag("-w", "--windowed", help="Do not start in fullscreen mode")
|
||||
flag("-v", "--verbose", help="Show more information")
|
||||
|
||||
try:
|
||||
let opts = p.parse(commandLineParams())
|
||||
|
||||
# Catch the help option. Do nothing more
|
||||
if opts.help:
|
||||
return
|
||||
|
||||
# Parse the starting mode
|
||||
var startMode: Mode
|
||||
try:
|
||||
startMode= parseEnum[Mode](opts.mode)
|
||||
except ValueError:
|
||||
startMode = Mode.None
|
||||
|
||||
# Create the image provider
|
||||
if opts.directoy != "":
|
||||
imageProvider = newImageProvider(opts.verbose, startMode, opts.directoy)
|
||||
else:
|
||||
imageProvider = newImageProvider(opts.verbose, startMode)
|
||||
|
||||
## Timeout is given in seconds as an argument
|
||||
var timeout = 3000
|
||||
try:
|
||||
timeout = opts.timeout.parseInt * 1000
|
||||
except ValueError:
|
||||
raise newException(UsageError, fmt"Invalid timeout value: {opts.timeout}")
|
||||
|
||||
return some(Args(
|
||||
fullscreen: not opts.windowed,
|
||||
verbose: opts.verbose,
|
||||
timeout: timeout,
|
||||
port: opts.port.parseInt))
|
||||
except:
|
||||
echo p.help
|
||||
|
||||
proc updateImage(image: Image): bool =
|
||||
## Updates the UI with a new image
|
||||
try:
|
||||
if args.verbose: log "Refreshing..."
|
||||
|
||||
if imageProvider.mode == Mode.None:
|
||||
log "No display mode"
|
||||
label.notify "No mode selected"
|
||||
|
||||
var wWidth, wHeight: int
|
||||
window.getSize(wWidth, wHeight)
|
||||
|
||||
let r = imageProvider.next(wWidth, wHeight)
|
||||
result = r.isOk
|
||||
if not r.isOk:
|
||||
label.notify r.error
|
||||
return
|
||||
|
||||
image.setFromFile(r.val)
|
||||
if imageProvider.mode != Mode.None:
|
||||
label.notify
|
||||
except:
|
||||
let
|
||||
e = getCurrentException()
|
||||
msg = getCurrentExceptionMsg()
|
||||
log "Got exception ", repr(e), " with message ", msg
|
||||
label.notify "Error while refreshing, retrying..."
|
||||
return false
|
||||
|
||||
proc timedUpdate(image: Image): bool =
|
||||
discard updateImage(image)
|
||||
# Force garbage collection now. Otherwise the RAM will fill up until the GC is triggered.
|
||||
GC_fullCollect()
|
||||
updateTimeout = int(timeoutAdd(uint32(args.timeout), timedUpdate, image))
|
||||
return false
|
||||
|
||||
proc forceUpdate(action: SimpleAction; parameter: Variant; image: Image): void =
|
||||
log "Refreshing..."
|
||||
label.notify "Refreshing..."
|
||||
if updateTimeout > 0:
|
||||
discard updateTimeout.remove
|
||||
updateTimeout = int(timeoutAdd(500, timedUpdate, image))
|
||||
|
||||
proc checkServerChannel(image: Image): bool =
|
||||
## Check the channel from the control server for incomming commands
|
||||
let tried = chan.tryRecv()
|
||||
|
||||
if tried.dataAvailable:
|
||||
let msg: CommandMessage = tried.msg
|
||||
log "Recieved command: ", msg.command
|
||||
|
||||
case msg.command
|
||||
of cRefresh:
|
||||
forceUpdate(nil, nil, image)
|
||||
|
||||
of cTimeout:
|
||||
let val = msg.parameter.parseInt * 1000
|
||||
log "Setting timeout to ", val
|
||||
args.timeout = val
|
||||
if updateTimeout > 0:
|
||||
discard updateTimeout.remove
|
||||
updateTimeout = int(timeoutAdd(uint32(args.timeout), timedUpdate, image))
|
||||
|
||||
of cMode:
|
||||
try:
|
||||
let mode = parseEnum[Mode](msg.parameter)
|
||||
imageProvider.mode = mode
|
||||
forceUpdate(nil, nil, image)
|
||||
log "Switching mode: ", mode
|
||||
label.notify fmt"Switch Mode: {msg.parameter.capitalizeAscii()}"
|
||||
except ValueError:
|
||||
log "Invalid mode: ", msg.parameter
|
||||
|
||||
else:
|
||||
log "Command ignored: ", msg.command
|
||||
result = true
|
||||
|
||||
proc toggleFullscreen(action: SimpleAction; parameter: Variant; window: ApplicationWindow) =
|
||||
## Fullscreen toggle event
|
||||
if args.fullscreen:
|
||||
window.unfullscreen
|
||||
else:
|
||||
window.fullscreen
|
||||
args.fullscreen = not args.fullscreen
|
||||
|
||||
proc toggleHelp(action: SimpleAction; parameter: Variant; box: Box) =
|
||||
if box.visible:
|
||||
box.hide
|
||||
else:
|
||||
box.show
|
||||
|
||||
proc cleanUp(w: ApplicationWindow, app: Application) =
|
||||
## Stop the control server and exit the GTK application
|
||||
chan.close()
|
||||
log "Server channel closed."
|
||||
app.quit()
|
||||
|
||||
proc quit(action: SimpleAction; parameter: Variant; app: Application) =
|
||||
## Application quit event
|
||||
cleanUp(window, app)
|
||||
|
||||
proc realizeWindow(window: ApplicationWindow, image: Image): void =
|
||||
## Hides the mouse pointer for the application.
|
||||
if args.fullscreen:
|
||||
window.fullscreen
|
||||
let cur = window.getDisplay().newCursorForDisplay(CursorType.blankCursor)
|
||||
let win = window.getWindow()
|
||||
win.setCursor(cur)
|
||||
|
||||
# Setting the inital image
|
||||
# Fix 1 second timeout to make sure all other initialization has finished
|
||||
updateTimeout = int(timeoutAdd(1000, timedUpdate, image))
|
||||
|
||||
proc appActivate(app: Application) =
|
||||
# Parse arguments from the command line
|
||||
let parsed = newArgs()
|
||||
if parsed.isNone:
|
||||
return
|
||||
else:
|
||||
args = parsed.get
|
||||
|
||||
window = newApplicationWindow(app)
|
||||
window.title = "randopix"
|
||||
window.setKeepAbove(false)
|
||||
window.setDefaultSize(800, 600)
|
||||
|
||||
# Custom styling for e.g. the background color CSS data is in the "style.nim" module
|
||||
let provider = newCssProvider()
|
||||
discard provider.loadFromData(css)
|
||||
addProviderForScreen(getDefaultScreen(), provider, STYLE_PROVIDER_PRIORITY_USER)
|
||||
|
||||
# Create all windgets we are gonna use
|
||||
label = newLabel(fmt"Starting ('H' for help)...")
|
||||
|
||||
let spinner = newSpinner()
|
||||
spinner.start()
|
||||
|
||||
box = newBox(Orientation.horizontal, 2)
|
||||
box.halign = Align.`end`
|
||||
box.valign = Align.`end`
|
||||
box.packStart(spinner, true, true, 10)
|
||||
box.packStart(label, true, true, 0)
|
||||
|
||||
let helpText = newLabel(helpString)
|
||||
let helpBox = newBox(Orientation.vertical, 0)
|
||||
helpBox.packStart(helpText, true, true, 0)
|
||||
helpBox.halign = Align.start
|
||||
helpBox.valign = Align.start
|
||||
|
||||
let container = newOverlay()
|
||||
container.addOverlay(box)
|
||||
container.addOverlay(helpBox)
|
||||
window.add(container)
|
||||
|
||||
let image = newImage()
|
||||
container.add(image)
|
||||
|
||||
## Connect the GTK signals to the procs
|
||||
var action: SimpleAction
|
||||
|
||||
action = newSimpleAction("fullscreen")
|
||||
discard action.connect("activate", toggleFullscreen, window)
|
||||
app.setAccelsForAction("win.fullscreen", "F")
|
||||
window.actionMap.addAction(action)
|
||||
|
||||
action = newSimpleAction("quit")
|
||||
discard action.connect("activate", quit, app)
|
||||
app.setAccelsForAction("win.quit", "Escape")
|
||||
window.actionMap.addAction(action)
|
||||
|
||||
action = newSimpleAction("update")
|
||||
discard action.connect("activate", forceUpdate, image)
|
||||
app.setAccelsForAction("win.update", "U")
|
||||
window.actionMap.addAction(action)
|
||||
|
||||
action = newSimpleAction("help")
|
||||
discard action.connect("activate", toggleHelp, helpBox)
|
||||
app.setAccelsForAction("win.help", "H")
|
||||
window.actionMap.addAction(action)
|
||||
|
||||
window.connect("destroy", cleanUp, app)
|
||||
window.connect("realize", realizeWindow, image)
|
||||
|
||||
window.showAll
|
||||
# Help is only shown on demand
|
||||
helpBox.hide
|
||||
|
||||
## open communication channel from the control server
|
||||
chan.open()
|
||||
|
||||
## Start the server for handling incoming commands
|
||||
let serverArgs = ServerArgs(verbose: args.verbose, port: args.port)
|
||||
createThread(serverWorker, runServer, serverArgs)
|
||||
serverTimeout = int(timeoutAdd(100, checkServerChannel, image))
|
||||
|
||||
when isMainModule:
|
||||
let app = newApplication("org.luxick.randopix")
|
||||
connect(app, "activate", appActivate)
|
||||
discard run(app)
|
||||
3
src/randopix.nim.cfg
Normal file
3
src/randopix.nim.cfg
Normal file
@@ -0,0 +1,3 @@
|
||||
threads:on
|
||||
d:ssl
|
||||
# gc:arc
|
||||
11
src/resources/app.css
Normal file
11
src/resources/app.css
Normal file
@@ -0,0 +1,11 @@
|
||||
window {
|
||||
background: black;
|
||||
font-size: 30px;
|
||||
}
|
||||
box {
|
||||
background-color: rgba(255, 255, 255, .75);
|
||||
border: 2px solid gray;
|
||||
border-radius: 5px;
|
||||
margin: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
BIN
src/resources/blank.png
Normal file
BIN
src/resources/blank.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
src/resources/www/favicon.ico
Normal file
BIN
src/resources/www/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/resources/www/github.png
Normal file
BIN
src/resources/www/github.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
53
src/resources/www/index.html
Normal file
53
src/resources/www/index.html
Normal file
@@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<title>randopix</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/site.css">
|
||||
|
||||
<script src="/pixctrl.js" type="text/javascript"></script>
|
||||
<script src="/script.js" type="text/javascript"></script>
|
||||
</head>
|
||||
|
||||
<body class="star-bg">
|
||||
<main>
|
||||
<h1 class="center">Randopix Remote</h1>
|
||||
|
||||
<p>Control the image that is shown on this randopix.</p>
|
||||
|
||||
<div class="control">
|
||||
<div class="control-info">Load the next image</div>
|
||||
<div class="control-body">
|
||||
<button type="button" class="btn" onclick="refresh()">Refresh Image Now!</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<div class="control-info">Switch the image mode</div>
|
||||
<div class="control-body">
|
||||
<select id="modeselect"></select>
|
||||
<button type="button" class="btn" onclick="js_setMode()">Set Mode</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<div class="control-info">Adjust timespan between images</div>
|
||||
<div class="control-body">
|
||||
<input id="timeout" type="number">
|
||||
<button type="button" class="btn" onclick="js_setTimeout()">Set Timeout</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="social">
|
||||
<a class="btn btn-mini" href="https://github.com/luxick/randopix">
|
||||
<img src="/github.png"></img>
|
||||
Source code and usage
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
BIN
src/resources/www/microfab.gif
Normal file
BIN
src/resources/www/microfab.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
src/resources/www/not-comic-sans.woff
Normal file
BIN
src/resources/www/not-comic-sans.woff
Normal file
Binary file not shown.
30
src/resources/www/script.js
Normal file
30
src/resources/www/script.js
Normal file
@@ -0,0 +1,30 @@
|
||||
function capitalize(string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
window.onload = () => {
|
||||
const modes = getModes();
|
||||
const modeselect = document.getElementById("modeselect");
|
||||
modes.forEach(mode => {
|
||||
let opt = document.createElement('option');
|
||||
opt.value = mode;
|
||||
opt.innerHTML = capitalize(mode)
|
||||
modeselect.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
function js_setTimeout() {
|
||||
const elem = document.getElementById("timeout");
|
||||
const timeout = parseInt(elem.value);
|
||||
if (timeout < 1) {
|
||||
console.error("timeout must be positive");
|
||||
return;
|
||||
}
|
||||
setTimeout(timeout.toString());
|
||||
elem.value = null;
|
||||
}
|
||||
|
||||
function js_setMode() {
|
||||
const modeselect = document.getElementById("modeselect");
|
||||
switchMode(modeselect.value)
|
||||
}
|
||||
157
src/resources/www/site.css
Normal file
157
src/resources/www/site.css
Normal file
@@ -0,0 +1,157 @@
|
||||
@charset "utf-8";
|
||||
/* CSS Document */
|
||||
|
||||
@font-face {
|
||||
font-family: notcomicsans;
|
||||
src: url("/not-comic-sans.woff") format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.star-bg {
|
||||
background-color: #1c1d1b;
|
||||
background-image: url("/stars.gif")
|
||||
}
|
||||
|
||||
/* Chrome, Safari, Opera */
|
||||
@-webkit-keyframes rainbow {
|
||||
0%{color: orange;}
|
||||
10%{color: purple;}
|
||||
20%{color: red;}
|
||||
30%{color: CadetBlue;}
|
||||
40%{color: yellow;}
|
||||
50%{color: coral;}
|
||||
60%{color: green;}
|
||||
70%{color: cyan;}
|
||||
80%{color: DeepPink;}
|
||||
90%{color: DodgerBlue;}
|
||||
100%{color: orange;}
|
||||
}
|
||||
|
||||
/* Internet Explorer */
|
||||
@-ms-keyframes rainbow {
|
||||
0%{color: orange;}
|
||||
10%{color: purple;}
|
||||
20%{color: red;}
|
||||
30%{color: CadetBlue;}
|
||||
40%{color: yellow;}
|
||||
50%{color: coral;}
|
||||
60%{color: green;}
|
||||
70%{color: cyan;}
|
||||
80%{color: DeepPink;}
|
||||
90%{color: DodgerBlue;}
|
||||
100%{color: orange;}
|
||||
}
|
||||
|
||||
/* Standard Syntax */
|
||||
@keyframes rainbow {
|
||||
0%{color: orange;}
|
||||
10%{color: purple;}
|
||||
20%{color: red;}
|
||||
30%{color: CadetBlue;}
|
||||
40%{color: yellow;}
|
||||
50%{color: coral;}
|
||||
60%{color: green;}
|
||||
70%{color: cyan;}
|
||||
80%{color: DeepPink;}
|
||||
90%{color: DodgerBlue;}
|
||||
100%{color: orange;}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: notcomicsans;
|
||||
color:#FFFF00;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 900px;
|
||||
align-self: center;
|
||||
border: 1px solid gray;
|
||||
border-radius: 5px;
|
||||
background-image: url("/microfab.gif");
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.control {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.control-info {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.control-body {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
}
|
||||
.control-body > * {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
/* Chrome, Safari, Opera */
|
||||
-webkit-animation: rainbow 5s infinite;
|
||||
/* Internet Explorer */
|
||||
-ms-animation: rainbow 5s infinite;
|
||||
/* Standar Syntax */
|
||||
animation: rainbow 5s infinite;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0;
|
||||
border: none;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
background-color: transparent;
|
||||
/* show a hand cursor on hover; some argue that we
|
||||
should keep the default arrow cursor for buttons */
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #ff0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
text-align: center;
|
||||
font-size: 1.3rem;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
border: 2px solid darkgray;
|
||||
border-radius: 5px;
|
||||
padding: 0px 10px;
|
||||
background-color: #1c1d1b;
|
||||
}
|
||||
.btn:hover {
|
||||
color: #ff0;
|
||||
fill: #ff0;
|
||||
background-color: #0b0c0b;
|
||||
}
|
||||
|
||||
.btn-mini {
|
||||
font-size: 0.75rem;
|
||||
border: 1px solid darkgray;
|
||||
padding: 3px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.btn-mini > img {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.social {
|
||||
margin-top: 20px;
|
||||
display: inline-block;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
BIN
src/resources/www/stars.gif
Normal file
BIN
src/resources/www/stars.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
87
src/server.nim
Normal file
87
src/server.nim
Normal file
@@ -0,0 +1,87 @@
|
||||
import asyncdispatch, strutils, json, logging, os
|
||||
import jester
|
||||
import common
|
||||
|
||||
when defined(release):
|
||||
proc slurpResources(): Table[string, string] {.compileTime.} =
|
||||
## Include everything from the www dir into the binary.
|
||||
## This way the final executable will not need an static web folder.
|
||||
for item in walkDir("src/resources/www/", true):
|
||||
if item.kind == pcFile:
|
||||
result[item.path] = slurp("resources/www/" & item.path)
|
||||
|
||||
const resources = slurpResources()
|
||||
|
||||
const
|
||||
contentTypes = {
|
||||
".js": "text/javascript",
|
||||
".css": "text/css"
|
||||
}.toTable
|
||||
|
||||
type
|
||||
ServerArgs* = object of RootObj
|
||||
verbose*: bool
|
||||
port*: int
|
||||
|
||||
var
|
||||
chan*: Channel[CommandMessage]
|
||||
verbose: bool
|
||||
|
||||
proc log(things: varargs[string, `$`]) =
|
||||
if verbose:
|
||||
echo things.join()
|
||||
|
||||
when defined(release):
|
||||
## When in release mode, use resources includes in the binary.
|
||||
## When developing use the files directly.
|
||||
router getRouter:
|
||||
get "/":
|
||||
resp resources["index.html"]
|
||||
|
||||
get "/@resource":
|
||||
try:
|
||||
var cType: string
|
||||
if contentTypes.hasKey(@"resource".splitFile.ext):
|
||||
cType = contentTypes[@"resource".splitFile.ext]
|
||||
resp resources[@"resource"], contentType=cType
|
||||
except KeyError:
|
||||
log "Resource not found: ", @"resource"
|
||||
resp Http404
|
||||
else:
|
||||
router getRouter:
|
||||
get "/":
|
||||
resp readFile("src/resources/www/index.html")
|
||||
get "/@resource":
|
||||
try:
|
||||
var cType: string
|
||||
if contentTypes.hasKey(@"resource".splitFile.ext):
|
||||
cType = contentTypes[@"resource".splitFile.ext]
|
||||
resp readFile("src/resources/www/" & @"resource"), contentType=cType
|
||||
except KeyError:
|
||||
log "Resource not found: ", @"resource"
|
||||
resp Http404
|
||||
|
||||
router postRouter:
|
||||
post "/":
|
||||
try:
|
||||
log "Command from ", request.ip
|
||||
let json = request.body.parseJson
|
||||
let msg = json.to(CommandMessage)
|
||||
log "Got message: ", $msg
|
||||
# Pass command from client to main applicaiton
|
||||
chan.send(msg)
|
||||
resp Http200
|
||||
except:
|
||||
log "Error: ", getCurrentExceptionMsg()
|
||||
|
||||
router randopixRouter:
|
||||
extend postRouter, ""
|
||||
extend getRouter, ""
|
||||
|
||||
proc runServer*[ServerArgs](arg: ServerArgs) {.thread, nimcall.} =
|
||||
verbose = arg.verbose
|
||||
logging.setLogFilter(lvlInfo)
|
||||
let port = Port(arg.port)
|
||||
let settings = newSettings(port=port)
|
||||
var server = initJester(randopixRouter, settings=settings)
|
||||
server.serve()
|
||||
1
src/server.nim.cfg
Normal file
1
src/server.nim.cfg
Normal file
@@ -0,0 +1 @@
|
||||
threads:on
|
||||
1
src/version
Normal file
1
src/version
Normal file
@@ -0,0 +1 @@
|
||||
1.1.0
|
||||
Reference in New Issue
Block a user