Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b39024091 | |||
| 9b8323349c | |||
| 82232f0ccf | |||
| 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 |
30
.github/workflows/build.yml
vendored
Normal file
30
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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
|
||||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -1 +1,12 @@
|
|||||||
bin/
|
# Ignore all
|
||||||
|
*
|
||||||
|
|
||||||
|
# Unignore all with extensions
|
||||||
|
!*.*
|
||||||
|
|
||||||
|
# Unignore all dirs
|
||||||
|
!*/
|
||||||
|
|
||||||
|
### Above combination will ignore all files without extension ###
|
||||||
|
bin/
|
||||||
|
.vscode/
|
||||||
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
|
||||||
|
```
|
||||||
@@ -6,13 +6,13 @@ 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"
|
||||||
bin = @["randopics"]
|
bin = @["randopix", "pixctrl"]
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
requires "nim >= 1.0.0", "gintro <= 0.5.5"
|
requires "nim >= 1.0.0", "gintro <= 0.5.5", "argparse >=0.10.1"
|
||||||
|
|
||||||
task debug, "Compile debug version":
|
task debug, "Compile debug version":
|
||||||
exec "nim c --out:bin/randopics src/randopics.nim"
|
exec "nim c -d:debug --debugger:native --out:randopix src/randopix.nim"
|
||||||
|
|
||||||
task release, "Compile release version":
|
task release, "Compile release version":
|
||||||
exec fmt"nim c -d:release --out:bin/{version}/randopics src/randopics.nim"
|
exec fmt"nim c -d:release --out:randopix-{version} src/randopix.nim"
|
||||||
11
src/app.css
Normal file
11
src/app.css
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
window {
|
||||||
|
background: black;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
background-color: #fff;
|
||||||
|
border: 2px solid gray;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
41
src/common.nim
Normal file
41
src/common.nim
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
const
|
||||||
|
defaultPort* = 5555 ## Default port at which the control server will run
|
||||||
|
|
||||||
|
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 newOpResult*(): OpResult =
|
||||||
|
OpResult(success: true)
|
||||||
|
|
||||||
|
proc newOpResult*(msg: string): OpResult =
|
||||||
|
OpResult(success: false, errorMsg: msg)
|
||||||
|
|
||||||
|
proc newCommand*(c: Command, p: string = ""): CommandMessage =
|
||||||
|
CommandMessage(command: c, parameter: p)
|
||||||
|
|
||||||
|
proc wrap*(msg: CommandMessage): string =
|
||||||
|
$(%msg) & "\r\L"
|
||||||
|
|
||||||
|
proc enumToStrings*(en: typedesc): seq[string] =
|
||||||
|
for x in en:
|
||||||
|
result.add $x
|
||||||
52
src/pixctrl.nim
Normal file
52
src/pixctrl.nim
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import strutils, net
|
||||||
|
import argparse
|
||||||
|
import common
|
||||||
|
|
||||||
|
const modeHelp = "Change the display mode. Possible values: [$1]" % Mode.enumToStrings().join(", ")
|
||||||
|
|
||||||
|
var socket = newSocket()
|
||||||
|
|
||||||
|
proc sendCommand*(server, port: string, msg: CommandMessage) =
|
||||||
|
socket.connect(server, Port(port.parseInt))
|
||||||
|
if not socket.trySend(msg.wrap):
|
||||||
|
echo "Cannot send command: ", msg
|
||||||
|
socket.close()
|
||||||
|
|
||||||
|
proc switchMode*(server, port: string, mode: string) =
|
||||||
|
try:
|
||||||
|
discard parseEnum[Mode](mode)
|
||||||
|
except ValueError:
|
||||||
|
echo "Invalid mode: ", mode
|
||||||
|
echo "Possible values: [$1]" % Mode.enumToStrings().join(", ")
|
||||||
|
return
|
||||||
|
let c = newCommand(cMode, mode)
|
||||||
|
sendCommand(server, port, c)
|
||||||
|
|
||||||
|
when isMainModule:
|
||||||
|
var p = newParser("pixctrl"):
|
||||||
|
help("Control utilitiy for randopix")
|
||||||
|
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):
|
||||||
|
help("Force image refresh now")
|
||||||
|
run:
|
||||||
|
let c = newCommand(cRefresh)
|
||||||
|
sendCommand(opts.parentOpts.server, opts.parentOpts.port, c)
|
||||||
|
|
||||||
|
command($cTimeout):
|
||||||
|
help("Set timeout in seconds before a new image is displayed")
|
||||||
|
arg("seconds", default = "300")
|
||||||
|
run:
|
||||||
|
let c = newCommand(cTimeout, opts.seconds)
|
||||||
|
sendCommand(opts.parentOpts.server, opts.parentOpts.port, c)
|
||||||
|
|
||||||
|
command($cMode):
|
||||||
|
help(modeHelp)
|
||||||
|
arg("mode")
|
||||||
|
run:
|
||||||
|
switchMode(opts.parentOpts.server, opts.parentOpts.port, opts.mode)
|
||||||
|
try:
|
||||||
|
p.run(commandLineParams())
|
||||||
|
except:
|
||||||
|
echo p.help
|
||||||
157
src/providers.nim
Normal file
157
src/providers.nim
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import os, sets, random, httpClient, json, strformat, options
|
||||||
|
import gintro/[gdkpixbuf, gobject]
|
||||||
|
import common
|
||||||
|
|
||||||
|
const
|
||||||
|
supportedExts = @[".png", ".jpg", ".jpeg"]
|
||||||
|
foxesUrl = "https://randomfox.ca/floof/"
|
||||||
|
inspiroUrl = "http://inspirobot.me/api?generate=true"
|
||||||
|
tmpFile = "/tmp/randopix_tmp.png"
|
||||||
|
|
||||||
|
type
|
||||||
|
FileOpResult* = object of OpResult
|
||||||
|
file*: string
|
||||||
|
|
||||||
|
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
|
||||||
|
files: seq[string] ## Currently loaded list of images in `file` mode
|
||||||
|
|
||||||
|
var
|
||||||
|
client = newHttpClient() ## For loading images from the web
|
||||||
|
|
||||||
|
########################
|
||||||
|
# Constructors
|
||||||
|
########################
|
||||||
|
|
||||||
|
proc newImageProvider(verbose: bool, mode: Mode, path: Option[string]): ImageProvider =
|
||||||
|
randomize()
|
||||||
|
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))
|
||||||
|
|
||||||
|
proc newFileOpResultError(msg: string): FileOpResult =
|
||||||
|
FileOpResult(success: false, errorMsg: msg)
|
||||||
|
|
||||||
|
proc newFileOpResult(file: string): FileOpResult =
|
||||||
|
FileOpResult(success: true, file: file)
|
||||||
|
|
||||||
|
########################
|
||||||
|
# Utilities
|
||||||
|
########################
|
||||||
|
|
||||||
|
proc log(ip: ImageProvider, msg: string) =
|
||||||
|
if ip.verbose: echo msg
|
||||||
|
|
||||||
|
########################
|
||||||
|
# Image Provider procs
|
||||||
|
########################
|
||||||
|
|
||||||
|
proc getFox(ip: ImageProvider): FileOpResult =
|
||||||
|
## 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)
|
||||||
|
return newFileOpResult(dlFile)
|
||||||
|
except JsonParsingError:
|
||||||
|
ip.log fmt"Error while fetching from fox API: {getCurrentExceptionMsg()}"
|
||||||
|
return newFileOpResultError("Json parsing error")
|
||||||
|
except KeyError:
|
||||||
|
ip.log fmt"No image in downloaded data: {getCurrentExceptionMsg()}"
|
||||||
|
return newFileOpResultError("No image from API")
|
||||||
|
|
||||||
|
proc getInspiro(ip: ImageProvider): FileOpResult =
|
||||||
|
## 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)
|
||||||
|
return newFileOpResult(dlFile)
|
||||||
|
except:
|
||||||
|
ip.log fmt"Unexpected error while downloading: {getCurrentExceptionMsg()}"
|
||||||
|
return newFileOpResultError(getCurrentExceptionMsg())
|
||||||
|
|
||||||
|
proc getLocalFile(ip: var ImageProvider): FileOpResult =
|
||||||
|
## 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 ip.files.len < 1:
|
||||||
|
if ip.path.isNone:
|
||||||
|
return newFileOpResultError("No path for image loading")
|
||||||
|
ip.log "Reloading file list..."
|
||||||
|
for file in walkDirRec(ip.path.get):
|
||||||
|
let split = splitFile(file)
|
||||||
|
if ip.exts.contains(split.ext):
|
||||||
|
ip.files.add(file)
|
||||||
|
ip.log fmt"Loaded {ip.files.len} files"
|
||||||
|
shuffle(ip.files)
|
||||||
|
|
||||||
|
# Remove the current file after
|
||||||
|
result = newFileOpResult(ip.files[0])
|
||||||
|
ip.files.delete(0)
|
||||||
|
|
||||||
|
proc getFileName(ip: var ImageProvider): FileOpResult =
|
||||||
|
## Get the temporary file name of the next file to display
|
||||||
|
case ip.mode
|
||||||
|
of Mode.File:
|
||||||
|
return ip.getLocalFile()
|
||||||
|
of Mode.Foxes:
|
||||||
|
return ip.getFox()
|
||||||
|
of Mode.Inspiro:
|
||||||
|
return ip.getInspiro()
|
||||||
|
else:
|
||||||
|
return newFileOpResultError("Not implemented")
|
||||||
|
|
||||||
|
########################
|
||||||
|
# Exported procs
|
||||||
|
########################
|
||||||
|
|
||||||
|
proc next*(ip: var ImageProvider, width, height: int): FileOpResult =
|
||||||
|
## Uses the image provider to get a new image ready to display.
|
||||||
|
## `width` and `height` should be the size of the window.
|
||||||
|
if ip.mode == Mode.None:
|
||||||
|
return newFileOpResultError("No mode active")
|
||||||
|
|
||||||
|
let op = ip.getFileName()
|
||||||
|
if not op.success: return op
|
||||||
|
|
||||||
|
var rawPixbuf = newPixbufFromFile(op.file)
|
||||||
|
# resize the pixbuf to best fit on screen
|
||||||
|
var w, h: int
|
||||||
|
if (width > height):
|
||||||
|
h = height
|
||||||
|
w = ((rawPixbuf.width * h) / rawPixbuf.height).toInt
|
||||||
|
else:
|
||||||
|
w = width
|
||||||
|
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
|
||||||
|
# directly setting the image from a pixbuf will leak memory
|
||||||
|
let saved = pixbuf.savev(tmpFile, "png", @[])
|
||||||
|
if not saved:
|
||||||
|
return newFileOpResultError("Error while saving temporary image")
|
||||||
|
|
||||||
|
# GTK pixbuf leaks memory when not manually decreasing reference count
|
||||||
|
pixbuf.genericGObjectUnref()
|
||||||
|
rawPixbuf.genericGObjectUnref()
|
||||||
|
|
||||||
|
newFileOpResult(tmpFile)
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import httpClient, json
|
|
||||||
import gintro/[gtk, glib, gobject, gio, gdkpixbuf]
|
|
||||||
|
|
||||||
var
|
|
||||||
client = newHttpClient()
|
|
||||||
window: ApplicationWindow
|
|
||||||
fullscreen = true
|
|
||||||
|
|
||||||
const
|
|
||||||
floofUrl = "https://randomfox.ca/floof/"
|
|
||||||
|
|
||||||
proc downloadImage(): Pixbuf =
|
|
||||||
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): Pixbuf =
|
|
||||||
var wWidth, wHeight, width, height: int
|
|
||||||
window.getSize(wWidth, wHeight)
|
|
||||||
|
|
||||||
if (wWidth > wHeight):
|
|
||||||
height = wHeight
|
|
||||||
width = ((pixbuf.width * height) / pixbuf.height).toInt
|
|
||||||
else:
|
|
||||||
width = wWidth
|
|
||||||
height = ((pixbuf.height * width) / pixbuf.width).toInt
|
|
||||||
|
|
||||||
pixbuf.scaleSimple(width, height, InterpType.bilinear)
|
|
||||||
|
|
||||||
proc updateImage(action: SimpleAction; parameter: Variant; widget: Image) =
|
|
||||||
var pixbuf = downloadImage()
|
|
||||||
pixbuf = pixbuf.resizeImage()
|
|
||||||
widget.setFromPixbuf(pixbuf)
|
|
||||||
|
|
||||||
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 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)
|
|
||||||
|
|
||||||
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 updateImageAction = newSimpleAction("update")
|
|
||||||
discard updateImageAction.connect("activate", updateImage, imageWidget)
|
|
||||||
app.setAccelsForAction("win.update", "U")
|
|
||||||
window.actionMap.addAction(updateImageAction)
|
|
||||||
|
|
||||||
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
|
|
||||||
246
src/randopix.nim
Normal file
246
src/randopix.nim
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
import os, options, strformat
|
||||||
|
import gintro/[glib, gobject, gtk, gio]
|
||||||
|
import gintro/gdk except Window
|
||||||
|
import argparse except run
|
||||||
|
import providers, server, common
|
||||||
|
|
||||||
|
const
|
||||||
|
css = slurp("app.css")
|
||||||
|
version = "0.1"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
var
|
||||||
|
imageProvider: ImageProvider ## Gets images from the chosen source
|
||||||
|
args: Args ## The parsed command line args
|
||||||
|
updateTimeout: int ## ID of the timeout that updates the images
|
||||||
|
# Widgets
|
||||||
|
window: ApplicationWindow
|
||||||
|
label: Label
|
||||||
|
# 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, message: string = "") =
|
||||||
|
## Shows the notification box in the lower left corner.
|
||||||
|
## If no message is passed, the box will be hidden
|
||||||
|
label.text = message
|
||||||
|
if (message == ""):
|
||||||
|
label.hide
|
||||||
|
else:
|
||||||
|
label.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("-p", "--path", help="Path to a directory with images for the 'file' mode")
|
||||||
|
option("-t", "--timeout", help="Seconds before the image is refreshed", default="300")
|
||||||
|
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.path != "":
|
||||||
|
imageProvider = newImageProvider(opts.verbose, startMode, opts.path)
|
||||||
|
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))
|
||||||
|
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"
|
||||||
|
return true
|
||||||
|
|
||||||
|
var wWidth, wHeight: int
|
||||||
|
window.getSize(wWidth, wHeight)
|
||||||
|
|
||||||
|
let op = imageProvider.next(wWidth, wHeight)
|
||||||
|
result = op.success
|
||||||
|
if not op.success:
|
||||||
|
label.notify op.errorMsg
|
||||||
|
return
|
||||||
|
|
||||||
|
image.setFromFile(op.file)
|
||||||
|
label.notify
|
||||||
|
except:
|
||||||
|
let
|
||||||
|
e = getCurrentException()
|
||||||
|
msg = getCurrentExceptionMsg()
|
||||||
|
log "Got exception ", repr(e), " with message ", msg
|
||||||
|
label.notify "Error while refreshing image, retrying..."
|
||||||
|
return false
|
||||||
|
|
||||||
|
proc timedUpdate(image: Image): bool =
|
||||||
|
discard updateImage(image);
|
||||||
|
updateTimeout = int(timeoutAdd(uint32(args.timeout), timedUpdate, image))
|
||||||
|
return false
|
||||||
|
|
||||||
|
proc forceUpdate(action: SimpleAction; parameter: Variant; image: Image): void =
|
||||||
|
log "Refreshing image..."
|
||||||
|
label.notify "Refreshing image..."
|
||||||
|
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
|
||||||
|
discard updateTimeout.remove
|
||||||
|
updateTimeout = int(timeoutAdd(uint32(args.timeout), timedUpdate, image))
|
||||||
|
|
||||||
|
of cMode:
|
||||||
|
try:
|
||||||
|
let mode = parseEnum[Mode](msg.parameter)
|
||||||
|
log "Switching mode: ", mode
|
||||||
|
imageProvider.mode = mode
|
||||||
|
except ValueError:
|
||||||
|
log "Invalid mode: ", msg.parameter
|
||||||
|
|
||||||
|
else:
|
||||||
|
log "Command ignored: ", msg.command
|
||||||
|
|
||||||
|
sleep(100)
|
||||||
|
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 cleanUp(w: ApplicationWindow, app: Application) =
|
||||||
|
## Stop the control server and exit the GTK application
|
||||||
|
log "Stopping control server..."
|
||||||
|
closeServer()
|
||||||
|
serverWorker.joinThread()
|
||||||
|
chan.close()
|
||||||
|
log "Server stopped."
|
||||||
|
app.quit()
|
||||||
|
|
||||||
|
proc quit(action: SimpleAction; parameter: Variant; app: Application) =
|
||||||
|
## Application quit event
|
||||||
|
cleanUp(window, app)
|
||||||
|
|
||||||
|
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("Starting...")
|
||||||
|
label.halign = Align.`end`
|
||||||
|
label.valign = Align.`end`
|
||||||
|
|
||||||
|
let container = newOverlay()
|
||||||
|
container.addOverlay(label)
|
||||||
|
window.add(container)
|
||||||
|
|
||||||
|
let image = newImage()
|
||||||
|
container.add(image)
|
||||||
|
|
||||||
|
if args.fullscreen:
|
||||||
|
window.fullscreen
|
||||||
|
|
||||||
|
## Connect the GTK signals to the procs
|
||||||
|
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 updateImageAction = newSimpleAction("update")
|
||||||
|
discard updateImageAction.connect("activate", forceUpdate, image)
|
||||||
|
app.setAccelsForAction("win.update", "U")
|
||||||
|
window.actionMap.addAction(updateImageAction)
|
||||||
|
|
||||||
|
window.connect("destroy", cleanUp, app)
|
||||||
|
|
||||||
|
window.showAll
|
||||||
|
|
||||||
|
# Setting the inital image
|
||||||
|
# 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
|
||||||
|
chan.open()
|
||||||
|
|
||||||
|
## Start the server for handling incoming commands
|
||||||
|
let serverArgs = newServerArgs(args.verbose)
|
||||||
|
createThread(serverWorker, runServer, serverArgs)
|
||||||
|
discard idleAdd(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
|
||||||
60
src/server.nim
Normal file
60
src/server.nim
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import net, json, strutils
|
||||||
|
import common
|
||||||
|
|
||||||
|
type
|
||||||
|
ServerArgs* = object of RootObj
|
||||||
|
verbose: bool
|
||||||
|
|
||||||
|
var
|
||||||
|
chan*: Channel[CommandMessage]
|
||||||
|
verbose: bool
|
||||||
|
|
||||||
|
proc newServerArgs*(verbose: bool): ServerArgs =
|
||||||
|
ServerArgs(verbose: verbose)
|
||||||
|
|
||||||
|
proc log(things: varargs[string, `$`]) =
|
||||||
|
if verbose:
|
||||||
|
echo things.join()
|
||||||
|
|
||||||
|
proc closeServer*() =
|
||||||
|
## Sends a "Close" command to the server
|
||||||
|
var socket = newSocket()
|
||||||
|
socket.connect("127.0.0.1", Port(defaultPort))
|
||||||
|
let c = newCommand(cClose)
|
||||||
|
socket.send(c.wrap)
|
||||||
|
socket.close()
|
||||||
|
|
||||||
|
proc runServer*[ServerArgs](arg: ServerArgs) {.thread, nimcall.} =
|
||||||
|
verbose = arg.verbose
|
||||||
|
var server = net.newSocket()
|
||||||
|
server.bindAddr(Port(defaultPort))
|
||||||
|
server.listen()
|
||||||
|
log "Control server is listening"
|
||||||
|
|
||||||
|
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/server.nim.cfg
Normal file
1
src/server.nim.cfg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
threads:on
|
||||||
Reference in New Issue
Block a user