Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
30
.github/workflows/build.yml
vendored
30
.github/workflows/build.yml
vendored
@@ -1,30 +0,0 @@
|
|||||||
name: Build randopix
|
|
||||||
# This workflow is triggered on pushes to the repository.
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- build
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build 64 Bit Linux
|
|
||||||
# This job runs on Linux
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Install GTK packages
|
|
||||||
run: sudo apt install gir1.2-gtk-3.0 gir1.2-graphene-1.0 gir1.2-gtksource-3.0 gir1.2-vte-2.91 gir1.2-notify-0.7 gir1.2-gst-plugins-bad-1.0
|
|
||||||
- uses: actions/checkout@master
|
|
||||||
- name: Cache choosenim
|
|
||||||
id: cache-choosenim
|
|
||||||
uses: actions/cache@v1
|
|
||||||
with:
|
|
||||||
path: ~/.choosenim
|
|
||||||
key: ${{ runner.os }}-choosenim-stable
|
|
||||||
- name: Cache nimble
|
|
||||||
id: cache-nimble
|
|
||||||
uses: actions/cache@v1
|
|
||||||
with:
|
|
||||||
path: ~/.nimble
|
|
||||||
key: ${{ runner.os }}-nimble-stable
|
|
||||||
- uses: jiro4989/setup-nim-action@v1.0.2
|
|
||||||
- run: nimble build -Y
|
|
||||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,12 +1,4 @@
|
|||||||
# Ignore all
|
|
||||||
*
|
|
||||||
|
|
||||||
# Unignore all with extensions
|
|
||||||
!*.*
|
|
||||||
|
|
||||||
# Unignore all dirs
|
|
||||||
!*/
|
|
||||||
|
|
||||||
### Above combination will ignore all files without extension ###
|
### Above combination will ignore all files without extension ###
|
||||||
bin/
|
bin/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
pixctrl.js
|
||||||
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 +1,29 @@
|
|||||||
import strformat
|
|
||||||
# Package
|
# Package
|
||||||
|
|
||||||
version = "0.1.0"
|
version = "1.1.0"
|
||||||
author = "luxick"
|
author = "luxick"
|
||||||
description = "Play an image slide show from different sources"
|
description = "Play an image slide show from different sources"
|
||||||
license = "GPL-2.0"
|
license = "GPL-2.0"
|
||||||
srcDir = "src"
|
srcDir = "src"
|
||||||
|
binDir = "bin"
|
||||||
bin = @["randopix", "pixctrl"]
|
bin = @["randopix", "pixctrl"]
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
requires "nim >= 1.0.0", "gintro <= 0.5.5", "argparse >=0.10.1"
|
requires "nim >= 1.0.0", "gintro", "argparse", "jester", "ajax"
|
||||||
|
|
||||||
|
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":
|
task debug, "Compile debug version":
|
||||||
exec "nim c -d:debug --debugger:native --out:randopix src/randopix.nim"
|
exec "nim c -d:debug --debugger:native -o:bin/randopix src/randopix.nim"
|
||||||
|
|
||||||
task release, "Compile release version":
|
before install:
|
||||||
exec fmt"nim c -d:release --out:randopix-{version} src/randopix.nim"
|
genJS()
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
const
|
|
||||||
defaultPort* = 5555 ## Default port at which the control server will run
|
|
||||||
|
|
||||||
type
|
type
|
||||||
OpResult* = object of RootObj ## Result object for signalling failure state across proc calls
|
OpResult* = object of RootObj ## Result object for signalling failure state across proc calls
|
||||||
success*: bool ## Indicating if the opration was successfull
|
success*: bool ## Indicating if the opration was successfull
|
||||||
@@ -24,18 +21,18 @@ type
|
|||||||
command*: Command ## Command that the application should execute
|
command*: Command ## Command that the application should execute
|
||||||
parameter*: string ## Optional parameter for the command
|
parameter*: string ## Optional parameter for the command
|
||||||
|
|
||||||
|
proc `$`(cMsg: CommandMessage): string =
|
||||||
|
$(%cMsg)
|
||||||
|
|
||||||
proc newOpResult*(): OpResult =
|
proc newOpResult*(): OpResult =
|
||||||
OpResult(success: true)
|
OpResult(success: true)
|
||||||
|
|
||||||
proc newOpResult*(msg: string): OpResult =
|
proc newOpResult*(msg: string): OpResult =
|
||||||
OpResult(success: false, errorMsg: msg)
|
OpResult(success: false, errorMsg: msg)
|
||||||
|
|
||||||
proc newCommand*(c: Command, p: string = ""): CommandMessage =
|
proc newCommandMessage*(c: Command, p: string = ""): CommandMessage =
|
||||||
CommandMessage(command: c, parameter: p)
|
CommandMessage(command: c, parameter: p)
|
||||||
|
|
||||||
proc wrap*(msg: CommandMessage): string =
|
|
||||||
$(%msg) & "\r\L"
|
|
||||||
|
|
||||||
proc enumToStrings*(en: typedesc): seq[string] =
|
proc enumToStrings*(en: typedesc): seq[string] =
|
||||||
for x in en:
|
for x in en:
|
||||||
result.add $x
|
result.add $x
|
||||||
114
src/pixctrl.nim
114
src/pixctrl.nim
@@ -1,52 +1,94 @@
|
|||||||
import strutils, net
|
import strutils, json
|
||||||
import argparse
|
|
||||||
import common
|
import common
|
||||||
|
|
||||||
const modeHelp = "Change the display mode. Possible values: [$1]" % Mode.enumToStrings().join(", ")
|
when defined(js):
|
||||||
|
import ajax, jsconsole, dom
|
||||||
|
else:
|
||||||
|
import httpClient, strformat
|
||||||
|
import argparse
|
||||||
|
var randopixServer* {.exportc.}: string ## URL for the randopix server
|
||||||
|
|
||||||
var socket = newSocket()
|
proc sendCommand(msg: CommandMessage) =
|
||||||
|
when defined(js):
|
||||||
|
console.log("Sending:", $msg, "to URL:", document.URL)
|
||||||
|
var req = newXMLHttpRequest()
|
||||||
|
|
||||||
proc sendCommand*(server, port: string, msg: CommandMessage) =
|
proc processSend(e:Event) =
|
||||||
socket.connect(server, Port(port.parseInt))
|
if req.readyState == rsDONE:
|
||||||
if not socket.trySend(msg.wrap):
|
if req.status != 200:
|
||||||
echo "Cannot send command: ", msg
|
console.log("There was a problem with the request.")
|
||||||
socket.close()
|
console.log($req.status, req.statusText)
|
||||||
|
|
||||||
proc switchMode*(server, port: string, mode: string) =
|
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:
|
try:
|
||||||
discard parseEnum[Mode](mode)
|
discard parseEnum[Mode](mode)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
echo "Invalid mode: ", mode
|
echo "Invalid mode: ", mode
|
||||||
echo "Possible values: [$1]" % Mode.enumToStrings().join(", ")
|
echo "Possible values: [$1]" % Mode.enumToStrings().join(", ")
|
||||||
return
|
return
|
||||||
let c = newCommand(cMode, mode)
|
sendCommand(newCommandMessage(cMode, mode))
|
||||||
sendCommand(server, port, c)
|
|
||||||
|
|
||||||
when isMainModule:
|
proc refresh*() {.exportc.} =
|
||||||
var p = newParser("pixctrl"):
|
## Force refresh of the current image
|
||||||
help("Control utilitiy for randopix")
|
sendCommand(cRefresh)
|
||||||
option("-s", "--server", help="Host running the randopix server", default="127.0.0.1")
|
|
||||||
option("-p", "--port", help="Port to connect to the randopix server", default = $defaultPort)
|
|
||||||
|
|
||||||
command($cRefresh):
|
proc setTimeout*(seconds: string) =
|
||||||
help("Force image refresh now")
|
## 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:
|
run:
|
||||||
let c = newCommand(cRefresh)
|
if opts.server.startsWith("http://"):
|
||||||
sendCommand(opts.parentOpts.server, opts.parentOpts.port, c)
|
randopixServer = opts.server
|
||||||
|
else:
|
||||||
|
randopixServer = fmt"http://{opts.server}"
|
||||||
|
|
||||||
command($cTimeout):
|
command($cRefresh):
|
||||||
help("Set timeout in seconds before a new image is displayed")
|
## Force refresh command
|
||||||
arg("seconds", default = "300")
|
help("Force image refresh now")
|
||||||
run:
|
run:
|
||||||
let c = newCommand(cTimeout, opts.seconds)
|
refresh()
|
||||||
sendCommand(opts.parentOpts.server, opts.parentOpts.port, c)
|
|
||||||
|
|
||||||
command($cMode):
|
command($cTimeout):
|
||||||
help(modeHelp)
|
## Timeout Command
|
||||||
arg("mode")
|
help("Set timeout in seconds before a new image is displayed")
|
||||||
run:
|
arg("seconds", default = "300")
|
||||||
switchMode(opts.parentOpts.server, opts.parentOpts.port, opts.mode)
|
run:
|
||||||
try:
|
setTimeout(opts.seconds)
|
||||||
p.run(commandLineParams())
|
|
||||||
except:
|
command($cMode):
|
||||||
echo p.help
|
## Mode switch command
|
||||||
|
help(modeHelp)
|
||||||
|
arg("mode")
|
||||||
|
run:
|
||||||
|
switchMode(opts.mode)
|
||||||
|
try:
|
||||||
|
p.run(commandLineParams())
|
||||||
|
except:
|
||||||
|
echo getCurrentExceptionMsg()
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import os, sets, random, httpClient, json, strformat, options
|
import os, sets, random, httpClient, json, strutils, strformat, options, deques, times
|
||||||
|
from lenientops import `*`
|
||||||
import gintro/[gdkpixbuf, gobject]
|
import gintro/[gdkpixbuf, gobject]
|
||||||
import common
|
import common
|
||||||
|
|
||||||
@@ -6,7 +7,6 @@ const
|
|||||||
supportedExts = @[".png", ".jpg", ".jpeg"]
|
supportedExts = @[".png", ".jpg", ".jpeg"]
|
||||||
foxesUrl = "https://randomfox.ca/floof/"
|
foxesUrl = "https://randomfox.ca/floof/"
|
||||||
inspiroUrl = "http://inspirobot.me/api?generate=true"
|
inspiroUrl = "http://inspirobot.me/api?generate=true"
|
||||||
tmpFile = "/tmp/randopix_tmp.png"
|
|
||||||
|
|
||||||
type
|
type
|
||||||
FileOpResult* = object of OpResult
|
FileOpResult* = object of OpResult
|
||||||
@@ -18,17 +18,18 @@ type
|
|||||||
mode* : Mode ## Selects the API that is used to get images
|
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
|
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
|
exts: HashSet[string] ## Allowed extensions that the `file` mode will display
|
||||||
files: seq[string] ## Currently loaded list of images in `file` mode
|
|
||||||
|
|
||||||
var
|
var
|
||||||
client = newHttpClient() ## For loading images from the web
|
client = newHttpClient() ## For loading images from the web
|
||||||
|
tmpDir = getTempDir() / "randopix"
|
||||||
|
tmpFile = tmpDir / "tmp.png"
|
||||||
|
fileList = initDeque[string]()
|
||||||
|
|
||||||
########################
|
########################
|
||||||
# Constructors
|
# Constructors
|
||||||
########################
|
########################
|
||||||
|
|
||||||
proc newImageProvider(verbose: bool, mode: Mode, path: Option[string]): ImageProvider =
|
proc newImageProvider(verbose: bool, mode: Mode, path: Option[string]): ImageProvider =
|
||||||
randomize()
|
|
||||||
ImageProvider(verbose: verbose, mode: mode, path: path, exts: supportedExts.toHashSet)
|
ImageProvider(verbose: verbose, mode: mode, path: path, exts: supportedExts.toHashSet)
|
||||||
|
|
||||||
proc newImageProvider*(verbose: bool): ImageProvider =
|
proc newImageProvider*(verbose: bool): ImageProvider =
|
||||||
@@ -53,8 +54,22 @@ proc newFileOpResult(file: string): FileOpResult =
|
|||||||
# Utilities
|
# Utilities
|
||||||
########################
|
########################
|
||||||
|
|
||||||
proc log(ip: ImageProvider, msg: string) =
|
proc log(ip: ImageProvider, things: varargs[string, `$`]) =
|
||||||
if ip.verbose: echo msg
|
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
|
# Image Provider procs
|
||||||
@@ -94,20 +109,24 @@ proc getLocalFile(ip: var ImageProvider): FileOpResult =
|
|||||||
|
|
||||||
# First, check if there are still images left to be loaded.
|
# First, check if there are still images left to be loaded.
|
||||||
# If not reread all files from the path
|
# If not reread all files from the path
|
||||||
if ip.files.len < 1:
|
if fileList.len == 0:
|
||||||
if ip.path.isNone:
|
var tmp: seq[string]
|
||||||
return newFileOpResultError("No path for image loading")
|
var split: tuple[dir, name, ext: string]
|
||||||
ip.log "Reloading file list..."
|
|
||||||
for file in walkDirRec(ip.path.get):
|
for file in walkDirRec(ip.path.get):
|
||||||
let split = splitFile(file)
|
split = splitFile(file)
|
||||||
if ip.exts.contains(split.ext):
|
if ip.exts.contains(split.ext):
|
||||||
ip.files.add(file)
|
tmp.add($file)
|
||||||
ip.log fmt"Loaded {ip.files.len} files"
|
|
||||||
shuffle(ip.files)
|
|
||||||
|
|
||||||
|
ip.log fmt"Loaded {tmp.len} files"
|
||||||
|
shuffle(tmp)
|
||||||
|
for file in tmp:
|
||||||
|
fileList.addLast(file)
|
||||||
|
if fileList.len == 0:
|
||||||
|
return newFileOpResultError("No files found")
|
||||||
|
|
||||||
|
let next = fileList.popFirst()
|
||||||
# Remove the current file after
|
# Remove the current file after
|
||||||
result = newFileOpResult(ip.files[0])
|
result = newFileOpResult(next)
|
||||||
ip.files.delete(0)
|
|
||||||
|
|
||||||
proc getFileName(ip: var ImageProvider): FileOpResult =
|
proc getFileName(ip: var ImageProvider): FileOpResult =
|
||||||
## Get the temporary file name of the next file to display
|
## Get the temporary file name of the next file to display
|
||||||
@@ -125,7 +144,7 @@ proc getFileName(ip: var ImageProvider): FileOpResult =
|
|||||||
# Exported procs
|
# Exported procs
|
||||||
########################
|
########################
|
||||||
|
|
||||||
proc next*(ip: var ImageProvider, width, height: int): FileOpResult =
|
proc next*(ip: var ImageProvider, maxWidth, maxHeight: int): FileOpResult =
|
||||||
## Uses the image provider to get a new image ready to display.
|
## Uses the image provider to get a new image ready to display.
|
||||||
## `width` and `height` should be the size of the window.
|
## `width` and `height` should be the size of the window.
|
||||||
if ip.mode == Mode.None:
|
if ip.mode == Mode.None:
|
||||||
@@ -135,15 +154,13 @@ proc next*(ip: var ImageProvider, width, height: int): FileOpResult =
|
|||||||
if not op.success: return op
|
if not op.success: return op
|
||||||
|
|
||||||
var rawPixbuf = newPixbufFromFile(op.file)
|
var rawPixbuf = newPixbufFromFile(op.file)
|
||||||
# resize the pixbuf to best fit on screen
|
# Resize the pixbuf to best fit on screen
|
||||||
var w, h: int
|
let size = calcImageSize(maxWidth, maxHeight, rawPixbuf.width, rawPixbuf.height)
|
||||||
if (width > height):
|
ip.log "Scale image to: ", size
|
||||||
h = height
|
let then = now()
|
||||||
w = ((rawPixbuf.width * h) / rawPixbuf.height).toInt
|
var pixbuf = rawPixbuf.scaleSimple(size.width, size.height, InterpType.nearest)
|
||||||
else:
|
let now = now()
|
||||||
w = width
|
ip.log "Image scaled. Time: ", (now - then).inMilliseconds, "ms"
|
||||||
h = ((rawPixbuf.height * w) / rawPixbuf.width).toInt
|
|
||||||
var pixbuf = rawPixbuf.scaleSimple(w, h, InterpType.bilinear)
|
|
||||||
# The pixbuf is written to disk and loaded again once because
|
# The pixbuf is written to disk and loaded again once because
|
||||||
# directly setting the image from a pixbuf will leak memory
|
# directly setting the image from a pixbuf will leak memory
|
||||||
let saved = pixbuf.savev(tmpFile, "png", @[])
|
let saved = pixbuf.savev(tmpFile, "png", @[])
|
||||||
@@ -151,7 +168,10 @@ proc next*(ip: var ImageProvider, width, height: int): FileOpResult =
|
|||||||
return newFileOpResultError("Error while saving temporary image")
|
return newFileOpResultError("Error while saving temporary image")
|
||||||
|
|
||||||
# GTK pixbuf leaks memory when not manually decreasing reference count
|
# GTK pixbuf leaks memory when not manually decreasing reference count
|
||||||
pixbuf.genericGObjectUnref()
|
pixbuf.unref()
|
||||||
rawPixbuf.genericGObjectUnref()
|
rawPixbuf.unref()
|
||||||
|
|
||||||
newFileOpResult(tmpFile)
|
newFileOpResult(tmpFile)
|
||||||
|
|
||||||
|
createDir(tmpDir)
|
||||||
|
randomize()
|
||||||
131
src/randopix.nim
131
src/randopix.nim
@@ -5,14 +5,21 @@ import argparse except run
|
|||||||
import providers, server, common
|
import providers, server, common
|
||||||
|
|
||||||
const
|
const
|
||||||
css = slurp("app.css")
|
css = slurp("resources/app.css")
|
||||||
version = "0.1"
|
version = slurp("version")
|
||||||
|
helpString = [
|
||||||
|
"ESC\tClose program",
|
||||||
|
"H\tShow/Hide this help",
|
||||||
|
"F\tToggle fullscreen",
|
||||||
|
"U\tForce refresh"
|
||||||
|
].join("\n")
|
||||||
|
|
||||||
type
|
type
|
||||||
Args = ref object
|
Args = ref object
|
||||||
fullscreen: bool ## Applicaion is show in fullscreen mode
|
fullscreen: bool ## Applicaion is show in fullscreen mode
|
||||||
verbose: bool ## More debug information in notification label
|
verbose: bool ## More debug information in notification label
|
||||||
timeout: int ## Milliseconds between image refreshes
|
timeout: int ## Milliseconds between image refreshes
|
||||||
|
port: int ## Port to host the control server
|
||||||
|
|
||||||
var
|
var
|
||||||
imageProvider: ImageProvider ## Gets images from the chosen source
|
imageProvider: ImageProvider ## Gets images from the chosen source
|
||||||
@@ -21,6 +28,7 @@ var
|
|||||||
# Widgets
|
# Widgets
|
||||||
window: ApplicationWindow
|
window: ApplicationWindow
|
||||||
label: Label
|
label: Label
|
||||||
|
box: Box
|
||||||
# Server vor recieving commands from external tools
|
# Server vor recieving commands from external tools
|
||||||
serverWorker: system.Thread[ServerArgs]
|
serverWorker: system.Thread[ServerArgs]
|
||||||
|
|
||||||
@@ -28,21 +36,22 @@ proc log(things: varargs[string, `$`]) =
|
|||||||
if args.verbose:
|
if args.verbose:
|
||||||
echo things.join()
|
echo things.join()
|
||||||
|
|
||||||
proc notify(label: Label, message: string = "") =
|
proc notify(label: Label, things: varargs[string, `$`]) =
|
||||||
## Shows the notification box in the lower left corner.
|
## Shows the notification box in the lower left corner.
|
||||||
## If no message is passed, the box will be hidden
|
## If no message is passed, the box will be hidden
|
||||||
label.text = message
|
label.text = things.join()
|
||||||
if (message == ""):
|
if (label.text == ""):
|
||||||
label.hide
|
box.hide
|
||||||
else:
|
else:
|
||||||
label.show
|
box.show
|
||||||
|
|
||||||
proc newArgs(): Option[Args] =
|
proc newArgs(): Option[Args] =
|
||||||
let p = newParser("randopix"):
|
let p = newParser("randopix"):
|
||||||
help(fmt"Version {version} - Display random images from different sources")
|
help(fmt"Version {version} - Display random images from different sources")
|
||||||
option("-m", "--mode", help="The image source mode.", choices=enumToStrings(Mode))
|
option("-m", "--mode", help="The image source mode.", choices=enumToStrings(Mode))
|
||||||
option("-p", "--path", help="Path to a directory with images for the 'file' 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("-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("-w", "--windowed", help="Do not start in fullscreen mode")
|
||||||
flag("-v", "--verbose", help="Show more information")
|
flag("-v", "--verbose", help="Show more information")
|
||||||
|
|
||||||
@@ -61,8 +70,8 @@ proc newArgs(): Option[Args] =
|
|||||||
startMode = Mode.None
|
startMode = Mode.None
|
||||||
|
|
||||||
# Create the image provider
|
# Create the image provider
|
||||||
if opts.path != "":
|
if opts.directoy != "":
|
||||||
imageProvider = newImageProvider(opts.verbose, startMode, opts.path)
|
imageProvider = newImageProvider(opts.verbose, startMode, opts.directoy)
|
||||||
else:
|
else:
|
||||||
imageProvider = newImageProvider(opts.verbose, startMode)
|
imageProvider = newImageProvider(opts.verbose, startMode)
|
||||||
|
|
||||||
@@ -76,7 +85,8 @@ proc newArgs(): Option[Args] =
|
|||||||
return some(Args(
|
return some(Args(
|
||||||
fullscreen: not opts.windowed,
|
fullscreen: not opts.windowed,
|
||||||
verbose: opts.verbose,
|
verbose: opts.verbose,
|
||||||
timeout: timeout))
|
timeout: timeout,
|
||||||
|
port: opts.port.parseInt))
|
||||||
except:
|
except:
|
||||||
echo p.help
|
echo p.help
|
||||||
|
|
||||||
@@ -106,17 +116,19 @@ proc updateImage(image: Image): bool =
|
|||||||
e = getCurrentException()
|
e = getCurrentException()
|
||||||
msg = getCurrentExceptionMsg()
|
msg = getCurrentExceptionMsg()
|
||||||
log "Got exception ", repr(e), " with message ", msg
|
log "Got exception ", repr(e), " with message ", msg
|
||||||
label.notify "Error while refreshing image, retrying..."
|
label.notify "Error while refreshing, retrying..."
|
||||||
return false
|
return false
|
||||||
|
|
||||||
proc timedUpdate(image: Image): bool =
|
proc timedUpdate(image: Image): bool =
|
||||||
discard updateImage(image);
|
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))
|
updateTimeout = int(timeoutAdd(uint32(args.timeout), timedUpdate, image))
|
||||||
return false
|
return false
|
||||||
|
|
||||||
proc forceUpdate(action: SimpleAction; parameter: Variant; image: Image): void =
|
proc forceUpdate(action: SimpleAction; parameter: Variant; image: Image): void =
|
||||||
log "Refreshing image..."
|
log "Refreshing..."
|
||||||
label.notify "Refreshing image..."
|
label.notify "Refreshing..."
|
||||||
if updateTimeout > 0:
|
if updateTimeout > 0:
|
||||||
discard updateTimeout.remove
|
discard updateTimeout.remove
|
||||||
updateTimeout = int(timeoutAdd(500, timedUpdate, image))
|
updateTimeout = int(timeoutAdd(500, timedUpdate, image))
|
||||||
@@ -137,14 +149,17 @@ proc checkServerChannel(image: Image): bool =
|
|||||||
let val = msg.parameter.parseInt * 1000
|
let val = msg.parameter.parseInt * 1000
|
||||||
log "Setting timeout to ", val
|
log "Setting timeout to ", val
|
||||||
args.timeout = val
|
args.timeout = val
|
||||||
discard updateTimeout.remove
|
if updateTimeout > 0:
|
||||||
|
discard updateTimeout.remove
|
||||||
updateTimeout = int(timeoutAdd(uint32(args.timeout), timedUpdate, image))
|
updateTimeout = int(timeoutAdd(uint32(args.timeout), timedUpdate, image))
|
||||||
|
|
||||||
of cMode:
|
of cMode:
|
||||||
try:
|
try:
|
||||||
let mode = parseEnum[Mode](msg.parameter)
|
let mode = parseEnum[Mode](msg.parameter)
|
||||||
log "Switching mode: ", mode
|
|
||||||
imageProvider.mode = mode
|
imageProvider.mode = mode
|
||||||
|
forceUpdate(nil, nil, image)
|
||||||
|
log "Switching mode: ", mode
|
||||||
|
label.notify fmt"Switch Mode: {msg.parameter.capitalizeAscii()}"
|
||||||
except ValueError:
|
except ValueError:
|
||||||
log "Invalid mode: ", msg.parameter
|
log "Invalid mode: ", msg.parameter
|
||||||
|
|
||||||
@@ -162,19 +177,34 @@ proc toggleFullscreen(action: SimpleAction; parameter: Variant; window: Applicat
|
|||||||
window.fullscreen
|
window.fullscreen
|
||||||
args.fullscreen = not args.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) =
|
proc cleanUp(w: ApplicationWindow, app: Application) =
|
||||||
## Stop the control server and exit the GTK application
|
## Stop the control server and exit the GTK application
|
||||||
log "Stopping control server..."
|
|
||||||
closeServer()
|
|
||||||
serverWorker.joinThread()
|
|
||||||
chan.close()
|
chan.close()
|
||||||
log "Server stopped."
|
log "Server channel closed."
|
||||||
app.quit()
|
app.quit()
|
||||||
|
|
||||||
proc quit(action: SimpleAction; parameter: Variant; app: Application) =
|
proc quit(action: SimpleAction; parameter: Variant; app: Application) =
|
||||||
## Application quit event
|
## Application quit event
|
||||||
cleanUp(window, app)
|
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) =
|
proc appActivate(app: Application) =
|
||||||
# Parse arguments from the command line
|
# Parse arguments from the command line
|
||||||
let parsed = newArgs()
|
let parsed = newArgs()
|
||||||
@@ -194,49 +224,66 @@ proc appActivate(app: Application) =
|
|||||||
addProviderForScreen(getDefaultScreen(), provider, STYLE_PROVIDER_PRIORITY_USER)
|
addProviderForScreen(getDefaultScreen(), provider, STYLE_PROVIDER_PRIORITY_USER)
|
||||||
|
|
||||||
# Create all windgets we are gonna use
|
# Create all windgets we are gonna use
|
||||||
label = newLabel("Starting...")
|
label = newLabel(fmt"Starting ('H' for help)...")
|
||||||
label.halign = Align.`end`
|
|
||||||
label.valign = Align.`end`
|
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()
|
let container = newOverlay()
|
||||||
container.addOverlay(label)
|
container.addOverlay(box)
|
||||||
|
container.addOverlay(helpBox)
|
||||||
window.add(container)
|
window.add(container)
|
||||||
|
|
||||||
let image = newImage()
|
let image = newImage()
|
||||||
container.add(image)
|
container.add(image)
|
||||||
|
|
||||||
if args.fullscreen:
|
|
||||||
window.fullscreen
|
|
||||||
|
|
||||||
## Connect the GTK signals to the procs
|
## Connect the GTK signals to the procs
|
||||||
let fullscreenAction = newSimpleAction("fullscreen")
|
var action: SimpleAction
|
||||||
discard fullscreenAction.connect("activate", toggleFullscreen, window)
|
|
||||||
|
action = newSimpleAction("fullscreen")
|
||||||
|
discard action.connect("activate", toggleFullscreen, window)
|
||||||
app.setAccelsForAction("win.fullscreen", "F")
|
app.setAccelsForAction("win.fullscreen", "F")
|
||||||
window.actionMap.addAction(fullscreenAction)
|
window.actionMap.addAction(action)
|
||||||
|
|
||||||
let quitAction = newSimpleAction("quit")
|
action = newSimpleAction("quit")
|
||||||
discard quitAction.connect("activate", quit, app)
|
discard action.connect("activate", quit, app)
|
||||||
app.setAccelsForAction("win.quit", "Escape")
|
app.setAccelsForAction("win.quit", "Escape")
|
||||||
window.actionMap.addAction(quitAction)
|
window.actionMap.addAction(action)
|
||||||
|
|
||||||
let updateImageAction = newSimpleAction("update")
|
action = newSimpleAction("update")
|
||||||
discard updateImageAction.connect("activate", forceUpdate, image)
|
discard action.connect("activate", forceUpdate, image)
|
||||||
app.setAccelsForAction("win.update", "U")
|
app.setAccelsForAction("win.update", "U")
|
||||||
window.actionMap.addAction(updateImageAction)
|
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("destroy", cleanUp, app)
|
||||||
|
window.connect("realize", realizeWindow, image)
|
||||||
|
|
||||||
window.showAll
|
window.showAll
|
||||||
|
# Help is only shown on demand
|
||||||
# Setting the inital image
|
helpBox.hide
|
||||||
# Fix 1 second timeout to make sure all other initialization has finished
|
|
||||||
updateTimeout = int(timeoutAdd(1000, timedUpdate, image))
|
|
||||||
|
|
||||||
## open communication channel from the control server
|
## open communication channel from the control server
|
||||||
chan.open()
|
chan.open()
|
||||||
|
|
||||||
## Start the server for handling incoming commands
|
## Start the server for handling incoming commands
|
||||||
let serverArgs = newServerArgs(args.verbose)
|
let serverArgs = ServerArgs(verbose: args.verbose, port: args.port)
|
||||||
createThread(serverWorker, runServer, serverArgs)
|
createThread(serverWorker, runServer, serverArgs)
|
||||||
discard idleAdd(checkServerChannel, image)
|
discard idleAdd(checkServerChannel, image)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
threads:on
|
threads:on
|
||||||
d:ssl
|
d:ssl
|
||||||
gc:arc
|
# gc:arc
|
||||||
@@ -2,8 +2,8 @@ window {
|
|||||||
background: black;
|
background: black;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
}
|
}
|
||||||
label {
|
box {
|
||||||
background-color: #fff;
|
background-color: rgba(255, 255, 255, .75);
|
||||||
border: 2px solid gray;
|
border: 2px solid gray;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
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 |
115
src/server.nim
115
src/server.nim
@@ -1,60 +1,87 @@
|
|||||||
import net, json, strutils
|
import asyncdispatch, strutils, json, logging, os
|
||||||
|
import jester
|
||||||
import common
|
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
|
type
|
||||||
ServerArgs* = object of RootObj
|
ServerArgs* = object of RootObj
|
||||||
verbose: bool
|
verbose*: bool
|
||||||
|
port*: int
|
||||||
|
|
||||||
var
|
var
|
||||||
chan*: Channel[CommandMessage]
|
chan*: Channel[CommandMessage]
|
||||||
verbose: bool
|
verbose: bool
|
||||||
|
|
||||||
proc newServerArgs*(verbose: bool): ServerArgs =
|
|
||||||
ServerArgs(verbose: verbose)
|
|
||||||
|
|
||||||
proc log(things: varargs[string, `$`]) =
|
proc log(things: varargs[string, `$`]) =
|
||||||
if verbose:
|
if verbose:
|
||||||
echo things.join()
|
echo things.join()
|
||||||
|
|
||||||
proc closeServer*() =
|
when defined(release):
|
||||||
## Sends a "Close" command to the server
|
## When in release mode, use resources includes in the binary.
|
||||||
var socket = newSocket()
|
## When developing use the files directly.
|
||||||
socket.connect("127.0.0.1", Port(defaultPort))
|
router getRouter:
|
||||||
let c = newCommand(cClose)
|
get "/":
|
||||||
socket.send(c.wrap)
|
resp resources["index.html"]
|
||||||
socket.close()
|
|
||||||
|
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.} =
|
proc runServer*[ServerArgs](arg: ServerArgs) {.thread, nimcall.} =
|
||||||
verbose = arg.verbose
|
verbose = arg.verbose
|
||||||
var server = net.newSocket()
|
logging.setLogFilter(lvlInfo)
|
||||||
server.bindAddr(Port(defaultPort))
|
let port = Port(arg.port)
|
||||||
server.listen()
|
let settings = newSettings(port=port)
|
||||||
log "Control server is listening"
|
var server = initJester(randopixRouter, settings=settings)
|
||||||
|
server.serve()
|
||||||
while true:
|
|
||||||
# Process client requests
|
|
||||||
var client = net.newSocket()
|
|
||||||
server.accept(client)
|
|
||||||
log "Client connected"
|
|
||||||
try:
|
|
||||||
var line = client.recvLine()
|
|
||||||
if line == "":
|
|
||||||
log "No data from client"
|
|
||||||
continue
|
|
||||||
|
|
||||||
var jsonData = parseJson(line)
|
|
||||||
let msg = jsonData.to(CommandMessage)
|
|
||||||
case msg.command
|
|
||||||
of cClose:
|
|
||||||
log "Server recieved termination command. Exiting."
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# Pass command from client to main applicaiton
|
|
||||||
chan.send(msg)
|
|
||||||
|
|
||||||
except OSError:
|
|
||||||
log "Server error: ", getCurrentExceptionMsg()
|
|
||||||
except:
|
|
||||||
log "Invalid command from client: ", getCurrentExceptionMsg()
|
|
||||||
log repr(getCurrentException())
|
|
||||||
server.close()
|
|
||||||
1
src/version
Normal file
1
src/version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1.1.0
|
||||||
Reference in New Issue
Block a user