1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2### BEGIN LICENSE
3# Copyright (c) 2012, Peter Levi <peterlevi@peterlevi.com>
4# This program is free software: you can redistribute it and/or modify it
5# under the terms of the GNU General Public License version 3, as published
6# by the Free Software Foundation.
7#
8# This program is distributed in the hope that it will be useful, but
9# WITHOUT ANY WARRANTY; without even the implied warranties of
10# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11# PURPOSE.  See the GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License along
14# with this program.  If not, see <http://www.gnu.org/licenses/>.
15### END LICENSE
16import hashlib
17import logging
18import os
19
20from configobj import ConfigObj, DuplicateError
21from variety.profile import get_profile_path
22from variety.Util import Util
23from variety_lib import varietyconfig
24
25logger = logging.getLogger("variety")
26
27TRUTH_VALUES = ["enabled", "1", "true", "on", "yes"]
28
29
30class Options:
31    OUTDATED_HASHES = {"clock_filter": ["dca6bd2dfa2b8c4e2db8801e39208f7f"]}
32    SIMPLE_DOWNLOADERS = []  # set by VarietyWindow at start
33    IMAGE_SOURCES = []  # set by VarietyWindow at start
34    CONFIGURABLE_IMAGE_SOURCES = []  # set by VarietyWindow at start
35    CONFIGURABLE_IMAGE_SOURCES_MAP = {}  # set by VarietyWindow at start
36
37    class SourceType:
38        # local files and folders
39        IMAGE = "image"
40        FOLDER = "folder"
41        ALBUM_FILENAME = "album (by filename)"
42        ALBUM_DATE = "album (by date)"
43
44        # special local folders
45        FAVORITES = "favorites"
46        FETCHED = "fetched"
47
48        # predefined configurable sources
49        FLICKR = "flickr"
50
51        BUILTIN_SOURCE_TYPES = {
52            IMAGE,
53            FOLDER,
54            ALBUM_FILENAME,
55            ALBUM_DATE,
56            FAVORITES,
57            FETCHED,
58            FLICKR,
59        }
60
61        LOCAL_PATH_TYPES = {IMAGE, FOLDER, ALBUM_FILENAME, ALBUM_DATE}
62
63        LOCAL_TYPES = {IMAGE, FOLDER, ALBUM_FILENAME, ALBUM_DATE, FAVORITES, FETCHED}
64
65        DL_TYPES = {FLICKR}
66
67        EDITABLE_DL_TYPES = {FLICKR}
68
69        REMOVABLE_TYPES = {FOLDER, IMAGE, ALBUM_FILENAME, ALBUM_DATE} | EDITABLE_DL_TYPES
70
71    class LightnessMode:
72        DARK = 0
73        LIGHT = 1
74
75    def __init__(self):
76        self.configfile = os.path.join(get_profile_path(), "variety.conf")
77
78    def read(self):
79        self.set_defaults()
80
81        try:
82            config = self.read_config()
83            needs_writing = self.fix_outdated(config)
84
85            try:
86                self.change_enabled = config["change_enabled"].lower() in TRUTH_VALUES
87            except Exception:
88                pass
89
90            try:
91                self.change_on_start = config["change_on_start"].lower() in TRUTH_VALUES
92            except Exception:
93                pass
94
95            try:
96                self.change_interval = int(config["change_interval"])
97                if self.change_interval < 5:
98                    self.change_interval = 5
99            except Exception:
100                pass
101
102            try:
103                self.safe_mode = config["safe_mode"].lower() in TRUTH_VALUES
104            except Exception:
105                pass
106
107            try:
108                self.download_folder = os.path.expanduser(config["download_folder"])
109            except Exception:
110                pass
111
112            try:
113                self.download_preference_ratio = max(
114                    0, min(1, float(config["download_preference_ratio"]))
115                )
116            except Exception:
117                pass
118
119            try:
120                self.quota_enabled = config["quota_enabled"].lower() in TRUTH_VALUES
121            except Exception:
122                pass
123
124            try:
125                self.quota_size = max(50, int(config["quota_size"]))
126            except Exception:
127                pass
128
129            try:
130                self.favorites_folder = os.path.expanduser(config["favorites_folder"])
131            except Exception:
132                pass
133
134            try:
135                favorites_ops_text = config["favorites_operations"]
136                self.favorites_operations = list(
137                    [x.strip().split(":") for x in favorites_ops_text.split(";") if x]
138                )
139            except Exception:
140                pass
141
142            try:
143                self.fetched_folder = os.path.expanduser(config["fetched_folder"])
144            except Exception:
145                pass
146
147            try:
148                self.clipboard_enabled = config["clipboard_enabled"].lower() in TRUTH_VALUES
149            except Exception:
150                pass
151
152            try:
153                self.clipboard_use_whitelist = (
154                    config["clipboard_use_whitelist"].lower() in TRUTH_VALUES
155                )
156            except Exception:
157                pass
158
159            try:
160                self.clipboard_hosts = config["clipboard_hosts"].lower().split(",")
161            except Exception:
162                pass
163
164            try:
165                icon = config["icon"]
166                if icon in ["Light", "Dark", "Current", "1", "2", "3", "4", "None"] or (
167                    os.access(icon, os.R_OK) and Util.is_image(icon)
168                ):
169                    self.icon = icon
170            except Exception:
171                pass
172
173            try:
174                self.desired_color_enabled = config["desired_color_enabled"].lower() in TRUTH_VALUES
175            except Exception:
176                pass
177
178            try:
179                self.desired_color = list(map(int, config["desired_color"].split()))
180                for i, x in enumerate(self.desired_color):
181                    self.desired_color[i] = max(0, min(255, x))
182            except Exception:
183                self.desired_color = None
184
185            try:
186                self.min_size_enabled = config["min_size_enabled"].lower() in TRUTH_VALUES
187            except Exception:
188                pass
189
190            try:
191                self.min_size = int(config["min_size"])
192                self.min_size = max(0, min(100, self.min_size))
193            except Exception:
194                pass
195
196            try:
197                self.use_landscape_enabled = config["use_landscape_enabled"].lower() in TRUTH_VALUES
198            except Exception:
199                pass
200
201            try:
202                self.lightness_enabled = config["lightness_enabled"].lower() in TRUTH_VALUES
203            except Exception:
204                pass
205
206            try:
207                self.lightness_mode = int(config["lightness_mode"])
208                self.lightness_mode = max(0, min(1, self.lightness_mode))
209            except Exception:
210                pass
211
212            try:
213                self.min_rating_enabled = config["min_rating_enabled"].lower() in TRUTH_VALUES
214            except Exception:
215                pass
216
217            try:
218                self.min_rating = int(config["min_rating"])
219                self.min_rating = max(1, min(5, self.min_rating))
220            except Exception:
221                pass
222
223            try:
224                self.smart_notice_shown = config["smart_notice_shown"].lower() in TRUTH_VALUES
225            except Exception:
226                pass
227
228            try:
229                self.smart_register_shown = config["smart_register_shown"].lower() in TRUTH_VALUES
230            except Exception:
231                pass
232
233            try:
234                self.stats_notice_shown = config["stats_notice_shown"].lower() in TRUTH_VALUES
235            except Exception:
236                pass
237
238            try:
239                self.smart_enabled = config["smart_enabled"].lower() in TRUTH_VALUES
240            except Exception:
241                pass
242
243            try:
244                self.sync_enabled = config["sync_enabled"].lower() in TRUTH_VALUES
245            except Exception:
246                pass
247
248            try:
249                self.stats_enabled = config["stats_enabled"].lower() in TRUTH_VALUES
250            except Exception:
251                pass
252
253            try:
254                self.copyto_enabled = config["copyto_enabled"].lower() in TRUTH_VALUES
255            except Exception:
256                pass
257
258            try:
259                self.copyto_folder = os.path.expanduser(config["copyto_folder"])
260            except Exception:
261                pass
262
263            try:
264                self.clock_enabled = config["clock_enabled"].lower() in TRUTH_VALUES
265            except Exception:
266                pass
267
268            try:
269                self.clock_filter = config["clock_filter"].strip()
270            except Exception:
271                pass
272
273            try:
274                self.clock_font = config["clock_font"]
275            except Exception:
276                pass
277
278            try:
279                self.clock_date_font = config["clock_date_font"]
280            except Exception:
281                pass
282
283            try:
284                self.quotes_enabled = config["quotes_enabled"].lower() in TRUTH_VALUES
285            except Exception:
286                pass
287
288            try:
289                self.quotes_font = config["quotes_font"]
290            except Exception:
291                pass
292
293            try:
294                self.quotes_text_color = list(map(int, config["quotes_text_color"].split()))
295                for i, x in enumerate(self.quotes_text_color):
296                    self.quotes_text_color[i] = max(0, min(255, x))
297            except Exception:
298                pass
299
300            try:
301                self.quotes_bg_color = list(map(int, config["quotes_bg_color"].split()))
302                for i, x in enumerate(self.quotes_bg_color):
303                    self.quotes_bg_color[i] = max(0, min(255, x))
304            except Exception:
305                pass
306
307            try:
308                self.quotes_bg_opacity = int(float(config["quotes_bg_opacity"]))
309                self.quotes_bg_opacity = max(0, min(100, self.quotes_bg_opacity))
310            except Exception:
311                pass
312
313            try:
314                self.quotes_text_shadow = config["quotes_text_shadow"].lower() in TRUTH_VALUES
315            except Exception:
316                pass
317
318            try:
319                self.quotes_text_color = list(map(int, config["quotes_text_color"].split()))
320                for i, x in enumerate(self.quotes_text_color):
321                    self.quotes_text_color[i] = max(0, min(255, x))
322            except Exception:
323                pass
324
325            try:
326                self.quotes_disabled_sources = config["quotes_disabled_sources"].strip().split("|")
327            except Exception:
328                pass
329
330            try:
331                self.quotes_tags = config["quotes_tags"]
332            except Exception:
333                pass
334
335            try:
336                self.quotes_authors = config["quotes_authors"]
337            except Exception:
338                pass
339
340            try:
341                self.quotes_change_enabled = config["quotes_change_enabled"].lower() in TRUTH_VALUES
342            except Exception:
343                pass
344
345            try:
346                self.quotes_change_interval = int(config["quotes_change_interval"])
347                if self.quotes_change_interval < 10:
348                    self.quotes_change_interval = 10
349            except Exception:
350                pass
351
352            try:
353                self.quotes_width = int(float(config["quotes_width"]))
354                self.quotes_width = max(0, min(100, self.quotes_width))
355            except Exception:
356                pass
357
358            try:
359                self.quotes_hpos = int(float(config["quotes_hpos"]))
360                self.quotes_hpos = max(0, min(100, self.quotes_hpos))
361            except Exception:
362                pass
363
364            try:
365                self.quotes_vpos = int(float(config["quotes_vpos"]))
366                self.quotes_vpos = max(0, min(100, self.quotes_vpos))
367            except Exception:
368                pass
369
370            try:
371                self.quotes_max_length = int(config["quotes_max_length"])
372                self.quotes_max_length = max(0, self.quotes_max_length)
373            except Exception:
374                pass
375
376            try:
377                self.quotes_favorites_file = os.path.expanduser(config["quotes_favorites_file"])
378            except Exception:
379                pass
380
381            try:
382                self.slideshow_sources_enabled = (
383                    config["slideshow_sources_enabled"].lower() in TRUTH_VALUES
384                )
385            except Exception:
386                pass
387
388            try:
389                self.slideshow_favorites_enabled = (
390                    config["slideshow_favorites_enabled"].lower() in TRUTH_VALUES
391                )
392            except Exception:
393                pass
394
395            try:
396                self.slideshow_downloads_enabled = (
397                    config["slideshow_downloads_enabled"].lower() in TRUTH_VALUES
398                )
399            except Exception:
400                pass
401
402            try:
403                self.slideshow_custom_enabled = (
404                    config["slideshow_custom_enabled"].lower() in TRUTH_VALUES
405                )
406            except Exception:
407                pass
408
409            try:
410                custom_path = config["slideshow_custom_folder"]
411                if custom_path in ("None", "Default") or not os.path.isdir(custom_path):
412                    self.slideshow_custom_folder = Util.get_xdg_pictures_folder()
413                else:
414                    self.slideshow_custom_folder = custom_path
415            except Exception:
416                pass
417
418            try:
419                slideshow_sort_order = config["slideshow_sort_order"]
420                if slideshow_sort_order in [
421                    "Random",
422                    "Name, asc",
423                    "Name, desc",
424                    "Date, asc",
425                    "Date, desc",
426                ]:
427                    self.slideshow_sort_order = slideshow_sort_order
428            except Exception:
429                pass
430
431            try:
432                self.slideshow_monitor = config["slideshow_monitor"]
433            except Exception:
434                pass
435
436            try:
437                slideshow_mode = config["slideshow_mode"]
438                if slideshow_mode in ["Fullscreen", "Desktop", "Maximized", "Window"]:
439                    self.slideshow_mode = slideshow_mode
440            except Exception:
441                pass
442
443            try:
444                self.slideshow_seconds = float(config["slideshow_seconds"])
445                self.slideshow_seconds = max(0.5, self.slideshow_seconds)
446            except Exception:
447                pass
448
449            try:
450                self.slideshow_fade = float(config["slideshow_fade"])
451                self.slideshow_fade = max(0, min(1, self.slideshow_fade))
452            except Exception:
453                pass
454
455            try:
456                self.slideshow_zoom = float(config["slideshow_zoom"])
457                self.slideshow_zoom = max(0, min(1, self.slideshow_zoom))
458            except Exception:
459                pass
460
461            try:
462                self.slideshow_pan = float(config["slideshow_pan"])
463                self.slideshow_pan = max(0, min(0.20, self.slideshow_pan))
464            except Exception:
465                pass
466
467            self.sources = []
468            if "sources" in config:
469                sources = config["sources"]
470                for v in sources.values():
471                    try:
472                        self.sources.append(Options.parse_source(v))
473                    except Exception:
474                        logger.debug(lambda: "Cannot parse source: " + v, exc_info=True)
475                        logger.info("Ignoring no longer supported source %s", v)
476
477            # automatically append sources for all simple downloaders we have
478            source_types = set(s[1] for s in self.sources)
479            for downloader in sorted(self.SIMPLE_DOWNLOADERS, key=lambda dl: dl.get_source_type()):
480                if downloader.get_source_type() not in source_types:
481                    self.sources.append(
482                        [True, downloader.get_source_type(), downloader.get_description()]
483                    )
484
485            self.parse_autosources()
486
487            if "filters" in config:
488                self.filters = []
489                filters = config["filters"]
490                for v in filters.values():
491                    try:
492                        self.filters.append(Options.parse_filter(v))
493                    except Exception:
494                        logger.exception(lambda: "Cannot parse filter: " + str(v))
495
496            self.parse_autofilters()
497
498            if needs_writing:
499                logger.info(lambda: "Some outdated settings were updated, writing the changes")
500                self.write()
501
502        except Exception:
503            logger.exception(lambda: "Could not read configuration:")
504
505    def fix_outdated(self, config):
506        changed = False
507        for key, outdated_hashes in Options.OUTDATED_HASHES.items():
508            if key in config:
509                current_hash = hashlib.md5(config[key].encode()).hexdigest()
510                if current_hash in outdated_hashes:
511                    # entry is outdated: delete it and use the default
512                    logger.warning(
513                        lambda: "Option " + key + " has an outdated value, using the new default"
514                    )
515                    changed = True
516                    del config[key]
517        return changed
518
519    def parse_autosources(self):
520        try:
521            with open(varietyconfig.get_data_file("config", "sources.txt"), encoding="utf8") as f:
522                for line in f:
523                    if not line.strip() or line.strip().startswith("#"):
524                        continue
525                    try:
526                        s = Options.parse_source(line.strip())
527                        if s[1] in [src[1] for src in self.sources]:
528                            continue
529                        self.sources.append(s)
530                    except Exception:
531                        logger.exception(lambda: "Cannot parse source in sources.txt: " + line)
532        except Exception:
533            logger.exception(lambda: "Cannot open sources.txt")
534
535    def parse_autofilters(self):
536        try:
537            with open(varietyconfig.get_data_file("config", "filters.txt"), encoding="utf8") as f:
538                for line in f:
539                    if not line.strip() or line.strip().startswith("#"):
540                        continue
541                    try:
542                        s = Options.parse_filter(line.strip())
543                        if not s[1].lower() in [f[1].lower() for f in self.filters]:
544                            self.filters.append(s)
545                    except Exception:
546                        logger.exception(lambda: "Cannot parse filter in filters.txt: " + line)
547        except Exception:
548            logger.exception(lambda: "Cannot open filters.txt")
549
550    @staticmethod
551    def parse_source(v):
552        s = v.strip().split("|")
553        enabled = s[0].lower() in TRUTH_VALUES
554        return [enabled, s[1], s[2]]
555
556    @staticmethod
557    def parse_filter(v):
558        s = v.strip().split("|")
559        enabled = s[0].lower() in TRUTH_VALUES
560        return [enabled, s[1], s[2]]
561
562    @staticmethod
563    def get_all_supported_source_types():
564        return Options.SourceType.BUILTIN_SOURCE_TYPES | Options.get_plugin_source_types()
565
566    @staticmethod
567    def get_downloader_source_types():
568        return Options.SourceType.DL_TYPES | Options.get_plugin_source_types()
569
570    @staticmethod
571    def get_editable_source_types():
572        return Options.SourceType.EDITABLE_DL_TYPES | Options.get_configurable_plugin_source_types()
573
574    @staticmethod
575    def get_removable_source_types():
576        return Options.SourceType.REMOVABLE_TYPES | Options.get_editable_source_types()
577
578    @staticmethod
579    def get_plugin_source_types():
580        return set(dl.get_source_type() for dl in Options.IMAGE_SOURCES)
581
582    @staticmethod
583    def get_configurable_plugin_source_types():
584        return set(dl.get_source_type() for dl in Options.CONFIGURABLE_IMAGE_SOURCES)
585
586    def set_defaults(self):
587        self.change_enabled = True
588        self.change_on_start = False
589        self.change_interval = 300
590        self.safe_mode = False
591
592        self.download_folder = os.path.join(get_profile_path(), "Downloaded")
593        self.download_preference_ratio = 0.9
594        self.quota_enabled = True
595        self.quota_size = 1000
596
597        self.favorites_folder = os.path.join(get_profile_path(), "Favorites")
598        self.favorites_operations = [
599            ["Downloaded", "Copy"],
600            ["Fetched", "Move"],
601            ["Others", "Copy"],
602        ]
603
604        self.fetched_folder = os.path.join(get_profile_path(), "Fetched")
605        self.clipboard_enabled = False
606        self.clipboard_use_whitelist = True
607        self.clipboard_hosts = "wallhaven.cc,ns223506.ovh.net,wallpapers.net,flickr.com,imgur.com,deviantart.com,interfacelift.com,vladstudio.com".split(
608            ","
609        )
610
611        self.icon = "Light"
612
613        self.desired_color_enabled = False
614        self.desired_color = None
615        self.min_size_enabled = False
616        self.min_size = 80
617        self.use_landscape_enabled = True
618        self.lightness_enabled = False
619        self.lightness_mode = Options.LightnessMode.DARK
620        self.min_rating_enabled = False
621        self.min_rating = 4
622
623        self.smart_notice_shown = False
624        self.smart_register_shown = False
625        self.stats_notice_shown = False
626
627        self.smart_enabled = False
628        self.sync_enabled = False
629        self.stats_enabled = False
630
631        self.copyto_enabled = False
632        self.copyto_folder = "Default"
633
634        self.clock_enabled = False
635        self.clock_font = "Ubuntu Condensed, 70"
636        self.clock_date_font = "Ubuntu Condensed, 30"
637        self.clock_filter = "-density 100 -font `fc-match -f '%{file[0]}' '%CLOCK_FONT_NAME'` -pointsize %CLOCK_FONT_SIZE -gravity SouthEast -fill '#00000044' -annotate 0x0+[%HOFFSET+58]+[%VOFFSET+108] '%H:%M' -fill white -annotate 0x0+[%HOFFSET+60]+[%VOFFSET+110] '%H:%M' -font `fc-match -f '%{file[0]}' '%DATE_FONT_NAME'` -pointsize %DATE_FONT_SIZE -fill '#00000044' -annotate 0x0+[%HOFFSET+58]+[%VOFFSET+58] '%A, %B %d' -fill white -annotate 0x0+[%HOFFSET+60]+[%VOFFSET+60] '%A, %B %d'"
638
639        self.quotes_enabled = False
640        self.quotes_font = "Bitstream Charter 30"
641        self.quotes_text_color = (255, 255, 255)
642        self.quotes_bg_color = (80, 80, 80)
643        self.quotes_bg_opacity = 55
644        self.quotes_text_shadow = False
645        self.quotes_disabled_sources = []
646        self.quotes_tags = ""
647        self.quotes_authors = ""
648        self.quotes_change_enabled = False
649        self.quotes_change_interval = 300
650        self.quotes_width = 70
651        self.quotes_hpos = 100
652        self.quotes_vpos = 40
653        self.quotes_max_length = 250
654        self.quotes_favorites_file = os.path.join(get_profile_path(), "favorite_quotes.txt")
655
656        self.slideshow_sources_enabled = True
657        self.slideshow_favorites_enabled = True
658        self.slideshow_downloads_enabled = False
659        self.slideshow_custom_enabled = False
660        self.slideshow_custom_folder = Util.get_xdg_pictures_folder()
661        self.slideshow_sort_order = "Random"
662        self.slideshow_monitor = "All"
663        self.slideshow_mode = "Fullscreen"
664        self.slideshow_seconds = 6
665        self.slideshow_fade = 0.4
666        self.slideshow_zoom = 0.2
667        self.slideshow_pan = 0.05
668
669        self.sources = [
670            [True, Options.SourceType.FAVORITES, "The Favorites folder"],
671            [True, Options.SourceType.FETCHED, "The Fetched folder"],
672            [True, Options.SourceType.FOLDER, "/usr/local/share/backgrounds/"],
673            [
674                True,
675                Options.SourceType.FLICKR,
676                "user:www.flickr.com/photos/peter-levi/;user_id:93647178@N00;",
677            ],
678        ]
679
680        self.filters = [
681            [False, "Keep original", ""],
682            [False, "Grayscale", "-type Grayscale"],
683            [False, "Heavy blur", "-blur 120x40"],
684            [False, "Oil painting", "-paint 6"],
685            [False, "Charcoal painting", "-charcoal 3"],
686            [False, "Pointilism", "-spread 10 -noise 3"],
687            [False, "Pixellate", "-scale 3% -scale 3333%"],
688        ]
689
690    def write(self):
691        try:
692            config = ConfigObj(self.configfile, encoding="utf8", default_encoding="utf8")
693        except Exception:
694            config = ConfigObj(encoding="utf8", default_encoding="utf8")
695            config.filename = self.configfile
696
697        try:
698            config["change_enabled"] = str(self.change_enabled)
699            config["change_on_start"] = str(self.change_on_start)
700            config["change_interval"] = str(self.change_interval)
701            config["safe_mode"] = str(self.safe_mode)
702
703            config["download_folder"] = Util.collapseuser(self.download_folder)
704            config["download_preference_ratio"] = str(self.download_preference_ratio)
705
706            config["quota_enabled"] = str(self.quota_enabled)
707            config["quota_size"] = str(self.quota_size)
708
709            config["favorites_folder"] = Util.collapseuser(self.favorites_folder)
710            config["favorites_operations"] = ";".join(
711                ":".join(x) for x in self.favorites_operations
712            )
713
714            config["fetched_folder"] = Util.collapseuser(self.fetched_folder)
715            config["clipboard_enabled"] = str(self.clipboard_enabled)
716            config["clipboard_use_whitelist"] = str(self.clipboard_use_whitelist)
717            config["clipboard_hosts"] = ",".join(self.clipboard_hosts)
718
719            config["icon"] = self.icon
720
721            config["desired_color_enabled"] = str(self.desired_color_enabled)
722            config["desired_color"] = (
723                " ".join(map(str, self.desired_color)) if self.desired_color else "None"
724            )
725            config["min_size_enabled"] = str(self.min_size_enabled)
726            config["min_size"] = str(self.min_size)
727            config["use_landscape_enabled"] = str(self.use_landscape_enabled)
728            config["lightness_enabled"] = str(self.lightness_enabled)
729            config["lightness_mode"] = str(self.lightness_mode)
730            config["min_rating_enabled"] = str(self.min_rating_enabled)
731            config["min_rating"] = str(self.min_rating)
732
733            config["smart_notice_shown"] = str(self.smart_notice_shown)
734            config["smart_register_shown"] = str(self.smart_register_shown)
735            config["stats_notice_shown"] = str(self.stats_notice_shown)
736
737            config["smart_enabled"] = str(self.smart_enabled)
738            config["sync_enabled"] = str(self.sync_enabled)
739            config["stats_enabled"] = str(self.stats_enabled)
740
741            config["copyto_enabled"] = str(self.copyto_enabled)
742            config["copyto_folder"] = Util.collapseuser(self.copyto_folder)
743
744            config["clock_enabled"] = str(self.clock_enabled)
745            config["clock_filter"] = self.clock_filter
746            config["clock_font"] = self.clock_font
747            config["clock_date_font"] = self.clock_date_font
748
749            config["quotes_enabled"] = str(self.quotes_enabled)
750            config["quotes_font"] = self.quotes_font
751            config["quotes_text_color"] = " ".join(map(str, self.quotes_text_color))
752            config["quotes_bg_color"] = " ".join(map(str, self.quotes_bg_color))
753            config["quotes_bg_opacity"] = str(self.quotes_bg_opacity)
754            config["quotes_text_shadow"] = str(self.quotes_text_shadow)
755            config["quotes_disabled_sources"] = "|".join(self.quotes_disabled_sources)
756            config["quotes_tags"] = self.quotes_tags
757            config["quotes_authors"] = self.quotes_authors
758            config["quotes_change_enabled"] = str(self.quotes_change_enabled)
759            config["quotes_change_interval"] = str(self.quotes_change_interval)
760            config["quotes_width"] = str(self.quotes_width)
761            config["quotes_hpos"] = str(self.quotes_hpos)
762            config["quotes_vpos"] = str(self.quotes_vpos)
763            config["quotes_max_length"] = str(self.quotes_max_length)
764            config["quotes_favorites_file"] = Util.collapseuser(self.quotes_favorites_file)
765
766            config["slideshow_sources_enabled"] = str(self.slideshow_sources_enabled)
767            config["slideshow_favorites_enabled"] = str(self.slideshow_favorites_enabled)
768            config["slideshow_downloads_enabled"] = str(self.slideshow_downloads_enabled)
769            config["slideshow_custom_enabled"] = str(self.slideshow_custom_enabled)
770            config["slideshow_custom_folder"] = Util.collapseuser(self.slideshow_custom_folder)
771            config["slideshow_sort_order"] = self.slideshow_sort_order
772            config["slideshow_monitor"] = self.slideshow_monitor
773            config["slideshow_mode"] = self.slideshow_mode
774            config["slideshow_seconds"] = str(self.slideshow_seconds)
775            config["slideshow_fade"] = str(self.slideshow_fade)
776            config["slideshow_zoom"] = str(self.slideshow_zoom)
777            config["slideshow_pan"] = str(self.slideshow_pan)
778
779            config["sources"] = {}
780            for i, s in enumerate(self.sources):
781                config["sources"]["src" + str(i + 1)] = str(s[0]) + "|" + str(s[1]) + "|" + s[2]
782
783            config["filters"] = {}
784            for i, f in enumerate(self.filters):
785                config["filters"]["filter" + str(i + 1)] = str(f[0]) + "|" + f[1] + "|" + f[2]
786
787            config.write()
788
789        except Exception:
790            logger.exception(lambda: "Could not write configuration:")
791
792    @staticmethod
793    def set_options(opts):
794        config = Options().read_config()
795        for key, value in opts:
796            config[key] = value
797        config.write()
798
799    def read_config(self):
800        config = ConfigObj(raise_errors=False, encoding="utf8", default_encoding="utf8")
801        config.filename = self.configfile
802        try:
803            config.reload()
804        except DuplicateError:
805            logger.warning(lambda: "Duplicate keys in config file, please fix this")
806        return config
807
808
809if __name__ == "__main__":
810    formatter = logging.Formatter("%(levelname)s:%(name)s: %(funcName)s() '%(message)s'")
811
812    logger = logging.getLogger("variety")
813    logger_sh = logging.StreamHandler()
814    logger_sh.setFormatter(formatter)
815    logger.addHandler(logger_sh)
816
817    o = Options()
818    o.read()
819    print(o.sources)
820    print(o.filters)
821    o.write()
822