Files
randopix/src/randopix.nim

293 lines
8.7 KiB
Nim

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("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: int ## ID of the timeout that updates the images
# 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"
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, 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
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 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)
discard idleAdd(checkServerChannel, image)
when isMainModule:
let app = newApplication("org.luxick.randopix")
connect(app, "activate", appActivate)
discard run(app)