Run black on all Python scripts!
This commit is contained in:
parent
fb6cfce656
commit
cd9cbcaa28
|
@ -20,27 +20,28 @@ class ScreenTime:
|
||||||
line["date"] = now.timestamp()
|
line["date"] = now.timestamp()
|
||||||
|
|
||||||
print("WROTE", line)
|
print("WROTE", line)
|
||||||
with open(self.csv_path, 'a') as typedfd:
|
with open(self.csv_path, "a") as typedfd:
|
||||||
writer = csv.DictWriter(typedfd, fieldnames=self.FIELDS)
|
writer = csv.DictWriter(typedfd, fieldnames=self.FIELDS)
|
||||||
writer.writerow(line)
|
writer.writerow(line)
|
||||||
|
|
||||||
def on_window_event(self, _: i3ipc.connection.Connection,
|
def on_window_event(
|
||||||
e: i3ipc.events.WindowEvent) -> None:
|
self, _: i3ipc.connection.Connection, e: i3ipc.events.WindowEvent
|
||||||
|
) -> None:
|
||||||
focused = self.i3.get_tree().find_focused()
|
focused = self.i3.get_tree().find_focused()
|
||||||
self.write({
|
self.write(
|
||||||
"type": "window_" + e.change,
|
{
|
||||||
"class": focused.window_class,
|
"type": "window_" + e.change,
|
||||||
"role": focused.window_role,
|
"class": focused.window_class,
|
||||||
"title": focused.window_title,
|
"role": focused.window_role,
|
||||||
"instance": focused.window_instance,
|
"title": focused.window_title,
|
||||||
})
|
"instance": focused.window_instance,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def on_mode_event(self, _: i3ipc.connection.Connection,
|
def on_mode_event(
|
||||||
e: i3ipc.events.ModeEvent) -> None:
|
self, _: i3ipc.connection.Connection, e: i3ipc.events.ModeEvent
|
||||||
self.write({
|
) -> None:
|
||||||
"type": "mode",
|
self.write({"type": "mode", "title": e.change})
|
||||||
"title": e.change
|
|
||||||
})
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.i3 = i3ipc.Connection()
|
self.i3 = i3ipc.Connection()
|
||||||
|
@ -48,11 +49,11 @@ class ScreenTime:
|
||||||
self.i3.on(i3ipc.Event.MODE, self.on_mode_event)
|
self.i3.on(i3ipc.Event.MODE, self.on_mode_event)
|
||||||
|
|
||||||
self.csv_path = os.path.join(
|
self.csv_path = os.path.join(
|
||||||
os.path.expanduser(
|
os.path.expanduser(os.getenv("XDG_CACHE_PATH", "~/.cache/")),
|
||||||
os.getenv('XDG_CACHE_PATH', '~/.cache/')),
|
"screentime.csv",
|
||||||
'screentime.csv')
|
)
|
||||||
if not os.path.isfile(self.csv_path):
|
if not os.path.isfile(self.csv_path):
|
||||||
with open(self.csv_path, 'w') as typedfd:
|
with open(self.csv_path, "w") as typedfd:
|
||||||
writer = csv.DictWriter(typedfd, fieldnames=self.FIELDS)
|
writer = csv.DictWriter(typedfd, fieldnames=self.FIELDS)
|
||||||
writer.writeheader()
|
writer.writeheader()
|
||||||
self.write({"type": "start"})
|
self.write({"type": "start"})
|
||||||
|
@ -61,6 +62,6 @@ class ScreenTime:
|
||||||
self.i3.main()
|
self.i3.main()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
ST = ScreenTime()
|
ST = ScreenTime()
|
||||||
ST.main()
|
ST.main()
|
||||||
|
|
|
@ -11,14 +11,23 @@ if __name__ == "__main__":
|
||||||
WORKSPACE_THEME = 0
|
WORKSPACE_THEME = 0
|
||||||
FOCUS_THEME = 3
|
FOCUS_THEME = 3
|
||||||
URGENT_THEME = 1
|
URGENT_THEME = 1
|
||||||
CUSTOM_SUFFIXES = '▲■'
|
CUSTOM_SUFFIXES = "▲■"
|
||||||
|
|
||||||
customNames = dict()
|
customNames = dict()
|
||||||
for i in range(len(CUSTOM_SUFFIXES)):
|
for i in range(len(CUSTOM_SUFFIXES)):
|
||||||
short = str(i+1)
|
short = str(i + 1)
|
||||||
full = short + ' ' + CUSTOM_SUFFIXES[i]
|
full = short + " " + CUSTOM_SUFFIXES[i]
|
||||||
customNames[short] = full
|
customNames[short] = full
|
||||||
Bar.addSectionAll(I3WorkspacesProvider(theme=WORKSPACE_THEME, themeFocus=FOCUS_THEME, themeUrgent=URGENT_THEME, themeMode=URGENT_THEME, customNames=customNames), BarGroupType.LEFT)
|
Bar.addSectionAll(
|
||||||
|
I3WorkspacesProvider(
|
||||||
|
theme=WORKSPACE_THEME,
|
||||||
|
themeFocus=FOCUS_THEME,
|
||||||
|
themeUrgent=URGENT_THEME,
|
||||||
|
themeMode=URGENT_THEME,
|
||||||
|
customNames=customNames,
|
||||||
|
),
|
||||||
|
BarGroupType.LEFT,
|
||||||
|
)
|
||||||
|
|
||||||
# TODO Middle
|
# TODO Middle
|
||||||
Bar.addSectionAll(MpdProvider(theme=7), BarGroupType.LEFT)
|
Bar.addSectionAll(MpdProvider(theme=7), BarGroupType.LEFT)
|
||||||
|
|
|
@ -11,7 +11,7 @@ import logging
|
||||||
import coloredlogs
|
import coloredlogs
|
||||||
import updaters
|
import updaters
|
||||||
|
|
||||||
coloredlogs.install(level='DEBUG', fmt='%(levelname)s %(message)s')
|
coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s")
|
||||||
log = logging.getLogger()
|
log = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
|
@ -62,12 +62,13 @@ class Bar:
|
||||||
Bar.running = True
|
Bar.running = True
|
||||||
Section.init()
|
Section.init()
|
||||||
|
|
||||||
cmd = ['lemonbar', '-b', '-a', '64']
|
cmd = ["lemonbar", "-b", "-a", "64"]
|
||||||
for font in Bar.FONTS:
|
for font in Bar.FONTS:
|
||||||
cmd += ["-f", "{}:size={}".format(font, Bar.FONTSIZE)]
|
cmd += ["-f", "{}:size={}".format(font, Bar.FONTSIZE)]
|
||||||
Bar.process = subprocess.Popen(cmd, stdin=subprocess.PIPE,
|
Bar.process = subprocess.Popen(
|
||||||
stdout=subprocess.PIPE)
|
cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE
|
||||||
Bar.stdoutThread = BarStdoutThread()
|
)
|
||||||
|
Bar.stdoutThread = BarStdoutThread()
|
||||||
Bar.stdoutThread.start()
|
Bar.stdoutThread.start()
|
||||||
|
|
||||||
# Debug
|
# Debug
|
||||||
|
@ -92,7 +93,7 @@ class Bar:
|
||||||
print(88)
|
print(88)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
i3.on('ipc_shutdown', doStop)
|
i3.on("ipc_shutdown", doStop)
|
||||||
i3.main()
|
i3.main()
|
||||||
except BaseException:
|
except BaseException:
|
||||||
print(93)
|
print(93)
|
||||||
|
@ -114,7 +115,7 @@ class Bar:
|
||||||
if function in Bar.actionsF2H.keys():
|
if function in Bar.actionsF2H.keys():
|
||||||
return Bar.actionsF2H[function]
|
return Bar.actionsF2H[function]
|
||||||
|
|
||||||
handle = '{:x}'.format(Bar.nextHandle).encode()
|
handle = "{:x}".format(Bar.nextHandle).encode()
|
||||||
Bar.nextHandle += 1
|
Bar.nextHandle += 1
|
||||||
|
|
||||||
Bar.actionsF2H[function] = handle
|
Bar.actionsF2H[function] = handle
|
||||||
|
@ -177,7 +178,7 @@ class Bar:
|
||||||
Bar.string += BarGroup.color(*Section.EMPTY)
|
Bar.string += BarGroup.color(*Section.EMPTY)
|
||||||
|
|
||||||
# print(Bar.string)
|
# print(Bar.string)
|
||||||
Bar.process.stdin.write(bytes(Bar.string + '\n', 'utf-8'))
|
Bar.process.stdin.write(bytes(Bar.string + "\n", "utf-8"))
|
||||||
Bar.process.stdin.flush()
|
Bar.process.stdin.flush()
|
||||||
|
|
||||||
|
|
||||||
|
@ -196,7 +197,7 @@ class BarGroup:
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
|
|
||||||
self.sections = list()
|
self.sections = list()
|
||||||
self.string = ''
|
self.string = ""
|
||||||
self.parts = []
|
self.parts = []
|
||||||
|
|
||||||
#: One of the sections that had their theme or visibility changed
|
#: One of the sections that had their theme or visibility changed
|
||||||
|
@ -220,11 +221,11 @@ class BarGroup:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def fgColor(color):
|
def fgColor(color):
|
||||||
return "%{F" + (color or '-') + "}"
|
return "%{F" + (color or "-") + "}"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def bgColor(color):
|
def bgColor(color):
|
||||||
return "%{B" + (color or '-') + "}"
|
return "%{B" + (color or "-") + "}"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def color(fg, bg):
|
def color(fg, bg):
|
||||||
|
@ -243,8 +244,9 @@ class BarGroup:
|
||||||
oSec = secs[s + 1] if s < lenS - 1 else None
|
oSec = secs[s + 1] if s < lenS - 1 else None
|
||||||
else:
|
else:
|
||||||
oSec = secs[s - 1] if s > 0 else None
|
oSec = secs[s - 1] if s > 0 else None
|
||||||
oTheme = Section.THEMES[oSec.theme] \
|
oTheme = (
|
||||||
if oSec is not None else Section.EMPTY
|
Section.THEMES[oSec.theme] if oSec is not None else Section.EMPTY
|
||||||
|
)
|
||||||
|
|
||||||
if self.groupType == BarGroupType.LEFT:
|
if self.groupType == BarGroupType.LEFT:
|
||||||
if s == 0:
|
if s == 0:
|
||||||
|
@ -263,7 +265,6 @@ class BarGroup:
|
||||||
parts.append(BarGroup.color(*theme))
|
parts.append(BarGroup.color(*theme))
|
||||||
parts.append(sec)
|
parts.append(sec)
|
||||||
|
|
||||||
|
|
||||||
# TODO OPTI Concatenate successive strings
|
# TODO OPTI Concatenate successive strings
|
||||||
self.parts = parts
|
self.parts = parts
|
||||||
|
|
||||||
|
@ -315,11 +316,26 @@ class Section:
|
||||||
# COLORS = ['#272822', '#383830', '#49483e', '#75715e', '#a59f85', '#f8f8f2',
|
# COLORS = ['#272822', '#383830', '#49483e', '#75715e', '#a59f85', '#f8f8f2',
|
||||||
# '#f5f4f1', '#f9f8f5', '#f92672', '#fd971f', '#f4bf75', '#a6e22e',
|
# '#f5f4f1', '#f9f8f5', '#f92672', '#fd971f', '#f4bf75', '#a6e22e',
|
||||||
# '#a1efe4', '#66d9ef', '#ae81ff', '#cc6633']
|
# '#a1efe4', '#66d9ef', '#ae81ff', '#cc6633']
|
||||||
COLORS = ['#181818', '#AB4642', '#A1B56C', '#F7CA88', '#7CAFC2', '#BA8BAF',
|
COLORS = [
|
||||||
'#86C1B9', '#D8D8D8', '#585858', '#AB4642', '#A1B56C', '#F7CA88',
|
"#181818",
|
||||||
'#7CAFC2', '#BA8BAF', '#86C1B9', '#F8F8F8']
|
"#AB4642",
|
||||||
FGCOLOR = '#F8F8F2'
|
"#A1B56C",
|
||||||
BGCOLOR = '#272822'
|
"#F7CA88",
|
||||||
|
"#7CAFC2",
|
||||||
|
"#BA8BAF",
|
||||||
|
"#86C1B9",
|
||||||
|
"#D8D8D8",
|
||||||
|
"#585858",
|
||||||
|
"#AB4642",
|
||||||
|
"#A1B56C",
|
||||||
|
"#F7CA88",
|
||||||
|
"#7CAFC2",
|
||||||
|
"#BA8BAF",
|
||||||
|
"#86C1B9",
|
||||||
|
"#F8F8F8",
|
||||||
|
]
|
||||||
|
FGCOLOR = "#F8F8F2"
|
||||||
|
BGCOLOR = "#272822"
|
||||||
|
|
||||||
THEMES = list()
|
THEMES = list()
|
||||||
EMPTY = (FGCOLOR, BGCOLOR)
|
EMPTY = (FGCOLOR, BGCOLOR)
|
||||||
|
@ -347,17 +363,18 @@ class Section:
|
||||||
|
|
||||||
if theme is None:
|
if theme is None:
|
||||||
theme = Section.lastChosenTheme
|
theme = Section.lastChosenTheme
|
||||||
Section.lastChosenTheme = (Section.lastChosenTheme + 1) \
|
Section.lastChosenTheme = (Section.lastChosenTheme + 1) % len(
|
||||||
% len(Section.THEMES)
|
Section.THEMES
|
||||||
|
)
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
|
|
||||||
#: Displayed text
|
#: Displayed text
|
||||||
self.curText = ''
|
self.curText = ""
|
||||||
#: Displayed text size
|
#: Displayed text size
|
||||||
self.curSize = 0
|
self.curSize = 0
|
||||||
|
|
||||||
#: Destination text
|
#: Destination text
|
||||||
self.dstText = Text(' ', Text(), ' ')
|
self.dstText = Text(" ", Text(), " ")
|
||||||
#: Destination size
|
#: Destination size
|
||||||
self.dstSize = 0
|
self.dstSize = 0
|
||||||
|
|
||||||
|
@ -367,13 +384,16 @@ class Section:
|
||||||
self.icon = self.ICON
|
self.icon = self.ICON
|
||||||
self.persistent = self.PERSISTENT
|
self.persistent = self.PERSISTENT
|
||||||
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
try:
|
try:
|
||||||
return "<{}><{}>{:01d}{}{:02d}/{:02d}" \
|
return "<{}><{}>{:01d}{}{:02d}/{:02d}".format(
|
||||||
.format(self.curText, self.dstText,
|
self.curText,
|
||||||
self.theme, "+" if self.visible else "-",
|
self.dstText,
|
||||||
self.curSize, self.dstSize)
|
self.theme,
|
||||||
|
"+" if self.visible else "-",
|
||||||
|
self.curSize,
|
||||||
|
self.dstSize,
|
||||||
|
)
|
||||||
except:
|
except:
|
||||||
return super().__str__()
|
return super().__str__()
|
||||||
|
|
||||||
|
@ -399,9 +419,15 @@ class Section:
|
||||||
elif isinstance(text, Text) and not len(text.elements):
|
elif isinstance(text, Text) and not len(text.elements):
|
||||||
text = None
|
text = None
|
||||||
|
|
||||||
self.dstText[0] = None if (text is None and not self.persistent) else ((' ' + self.icon + ' ') if self.icon else ' ')
|
self.dstText[0] = (
|
||||||
|
None
|
||||||
|
if (text is None and not self.persistent)
|
||||||
|
else ((" " + self.icon + " ") if self.icon else " ")
|
||||||
|
)
|
||||||
self.dstText[1] = text
|
self.dstText[1] = text
|
||||||
self.dstText[2] = ' ' if self.dstText[1] is not None and len(self.dstText[1]) else None
|
self.dstText[2] = (
|
||||||
|
" " if self.dstText[1] is not None and len(self.dstText[1]) else None
|
||||||
|
)
|
||||||
|
|
||||||
self.dstSize = len(self.dstText)
|
self.dstSize = len(self.dstText)
|
||||||
self.dstText.setSection(self)
|
self.dstText.setSection(self)
|
||||||
|
@ -481,7 +507,7 @@ class Section:
|
||||||
elif p < 0:
|
elif p < 0:
|
||||||
return ramp[0]
|
return ramp[0]
|
||||||
else:
|
else:
|
||||||
return ramp[round(p * (len(ramp)-1))]
|
return ramp[round(p * (len(ramp) - 1))]
|
||||||
|
|
||||||
|
|
||||||
class StatefulSection(Section):
|
class StatefulSection(Section):
|
||||||
|
@ -492,10 +518,11 @@ class StatefulSection(Section):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
Section.__init__(self, *args, **kwargs)
|
Section.__init__(self, *args, **kwargs)
|
||||||
self.state = self.DEFAULT_STATE
|
self.state = self.DEFAULT_STATE
|
||||||
if hasattr(self, 'onChangeState'):
|
if hasattr(self, "onChangeState"):
|
||||||
self.onChangeState(self.state)
|
self.onChangeState(self.state)
|
||||||
self.setDecorators(clickLeft=self.incrementState,
|
self.setDecorators(
|
||||||
clickRight=self.decrementState)
|
clickLeft=self.incrementState, clickRight=self.decrementState
|
||||||
|
)
|
||||||
|
|
||||||
def incrementState(self):
|
def incrementState(self):
|
||||||
newState = min(self.state + 1, self.NUMBER_STATES - 1)
|
newState = min(self.state + 1, self.NUMBER_STATES - 1)
|
||||||
|
@ -509,16 +536,17 @@ class StatefulSection(Section):
|
||||||
assert isinstance(state, int)
|
assert isinstance(state, int)
|
||||||
assert state < self.NUMBER_STATES
|
assert state < self.NUMBER_STATES
|
||||||
self.state = state
|
self.state = state
|
||||||
if hasattr(self, 'onChangeState'):
|
if hasattr(self, "onChangeState"):
|
||||||
self.onChangeState(state)
|
self.onChangeState(state)
|
||||||
self.refreshData()
|
self.refreshData()
|
||||||
|
|
||||||
|
|
||||||
class ColorCountsSection(StatefulSection):
|
class ColorCountsSection(StatefulSection):
|
||||||
# TODO FEAT Blend colors when not expanded
|
# TODO FEAT Blend colors when not expanded
|
||||||
# TODO FEAT Blend colors with importance of count
|
# TODO FEAT Blend colors with importance of count
|
||||||
# TODO FEAT Allow icons instead of counts
|
# TODO FEAT Allow icons instead of counts
|
||||||
NUMBER_STATES = 3
|
NUMBER_STATES = 3
|
||||||
COLORABLE_ICON = '?'
|
COLORABLE_ICON = "?"
|
||||||
|
|
||||||
def __init__(self, theme=None):
|
def __init__(self, theme=None):
|
||||||
StatefulSection.__init__(self, theme=theme)
|
StatefulSection.__init__(self, theme=theme)
|
||||||
|
@ -538,12 +566,12 @@ class ColorCountsSection(StatefulSection):
|
||||||
# Icon + Total
|
# Icon + Total
|
||||||
elif self.state == 1 and len(counts) > 1:
|
elif self.state == 1 and len(counts) > 1:
|
||||||
total = sum([count for count, color in counts])
|
total = sum([count for count, color in counts])
|
||||||
return Text(self.COLORABLE_ICON, ' ', total)
|
return Text(self.COLORABLE_ICON, " ", total)
|
||||||
# Icon + Counts
|
# Icon + Counts
|
||||||
else:
|
else:
|
||||||
text = Text(self.COLORABLE_ICON)
|
text = Text(self.COLORABLE_ICON)
|
||||||
for count, color in counts:
|
for count, color in counts:
|
||||||
text.append(' ', Text(count, fg=color))
|
text.append(" ", Text(count, fg=color))
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
@ -586,12 +614,12 @@ class Text:
|
||||||
if self.prefix is not None and self.suffix is not None:
|
if self.prefix is not None and self.suffix is not None:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.prefix = ''
|
self.prefix = ""
|
||||||
self.suffix = ''
|
self.suffix = ""
|
||||||
|
|
||||||
def nest(prefix, suffix):
|
def nest(prefix, suffix):
|
||||||
self.prefix = self.prefix + '%{' + prefix + '}'
|
self.prefix = self.prefix + "%{" + prefix + "}"
|
||||||
self.suffix = '%{' + suffix + '}' + self.suffix
|
self.suffix = "%{" + suffix + "}" + self.suffix
|
||||||
|
|
||||||
def getColor(val):
|
def getColor(val):
|
||||||
# TODO Allow themes
|
# TODO Allow themes
|
||||||
|
@ -600,27 +628,27 @@ class Text:
|
||||||
|
|
||||||
def button(number, function):
|
def button(number, function):
|
||||||
handle = Bar.getFunctionHandle(function)
|
handle = Bar.getFunctionHandle(function)
|
||||||
nest('A' + number + ':' + handle.decode() + ':', 'A' + number)
|
nest("A" + number + ":" + handle.decode() + ":", "A" + number)
|
||||||
|
|
||||||
for key, val in self.decorators.items():
|
for key, val in self.decorators.items():
|
||||||
if val is None:
|
if val is None:
|
||||||
continue
|
continue
|
||||||
if key == 'fg':
|
if key == "fg":
|
||||||
reset = self.section.THEMES[self.section.theme][0]
|
reset = self.section.THEMES[self.section.theme][0]
|
||||||
nest('F' + getColor(val), 'F' + reset)
|
nest("F" + getColor(val), "F" + reset)
|
||||||
elif key == 'bg':
|
elif key == "bg":
|
||||||
reset = self.section.THEMES[self.section.theme][1]
|
reset = self.section.THEMES[self.section.theme][1]
|
||||||
nest('B' + getColor(val), 'B' + reset)
|
nest("B" + getColor(val), "B" + reset)
|
||||||
elif key == "clickLeft":
|
elif key == "clickLeft":
|
||||||
button('1', val)
|
button("1", val)
|
||||||
elif key == "clickMiddle":
|
elif key == "clickMiddle":
|
||||||
button('2', val)
|
button("2", val)
|
||||||
elif key == "clickRight":
|
elif key == "clickRight":
|
||||||
button('3', val)
|
button("3", val)
|
||||||
elif key == "scrollUp":
|
elif key == "scrollUp":
|
||||||
button('4', val)
|
button("4", val)
|
||||||
elif key == "scrollDown":
|
elif key == "scrollDown":
|
||||||
button('5', val)
|
button("5", val)
|
||||||
else:
|
else:
|
||||||
log.warn("Unkown decorator: {}".format(key))
|
log.warn("Unkown decorator: {}".format(key))
|
||||||
|
|
||||||
|
@ -652,7 +680,7 @@ class Text:
|
||||||
curString += self.suffix
|
curString += self.suffix
|
||||||
|
|
||||||
if pad and remSize > 0:
|
if pad and remSize > 0:
|
||||||
curString += ' ' * remSize
|
curString += " " * remSize
|
||||||
curSize += remSize
|
curSize += remSize
|
||||||
|
|
||||||
if size is not None:
|
if size is not None:
|
||||||
|
@ -688,7 +716,6 @@ class Text:
|
||||||
curSize += len(str(element))
|
curSize += len(str(element))
|
||||||
return curSize
|
return curSize
|
||||||
|
|
||||||
|
|
||||||
def __getitem__(self, index):
|
def __getitem__(self, index):
|
||||||
return self.elements[index]
|
return self.elements[index]
|
||||||
|
|
||||||
|
|
|
@ -7,150 +7,188 @@ Debugging script
|
||||||
import i3ipc
|
import i3ipc
|
||||||
import os
|
import os
|
||||||
import psutil
|
import psutil
|
||||||
|
|
||||||
# import alsaaudio
|
# import alsaaudio
|
||||||
from time import time
|
from time import time
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
i3 = i3ipc.Connection()
|
i3 = i3ipc.Connection()
|
||||||
lemonbar = subprocess.Popen(['lemonbar', '-b'], stdin=subprocess.PIPE)
|
lemonbar = subprocess.Popen(["lemonbar", "-b"], stdin=subprocess.PIPE)
|
||||||
|
|
||||||
# Utils
|
# Utils
|
||||||
def upChart(p):
|
def upChart(p):
|
||||||
block = ' ▁▂▃▄▅▆▇█'
|
block = " ▁▂▃▄▅▆▇█"
|
||||||
return block[round(p * (len(block)-1))]
|
return block[round(p * (len(block) - 1))]
|
||||||
|
|
||||||
def humanSizeOf(num, suffix='B'): # TODO Credit
|
|
||||||
for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']:
|
def humanSizeOf(num, suffix="B"): # TODO Credit
|
||||||
|
for unit in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]:
|
||||||
if abs(num) < 1024.0:
|
if abs(num) < 1024.0:
|
||||||
return "%3.0f%2s%s" % (num, unit, suffix)
|
return "%3.0f%2s%s" % (num, unit, suffix)
|
||||||
num /= 1024.0
|
num /= 1024.0
|
||||||
return "%.0f%2s%s" % (num, 'Yi', suffix)
|
return "%.0f%2s%s" % (num, "Yi", suffix)
|
||||||
|
|
||||||
|
|
||||||
# Values
|
# Values
|
||||||
mode = ''
|
mode = ""
|
||||||
container = i3.get_tree().find_focused()
|
container = i3.get_tree().find_focused()
|
||||||
workspaces = i3.get_workspaces()
|
workspaces = i3.get_workspaces()
|
||||||
outputs = i3.get_outputs()
|
outputs = i3.get_outputs()
|
||||||
|
|
||||||
username = os.environ['USER']
|
username = os.environ["USER"]
|
||||||
hostname = os.environ['HOSTNAME']
|
hostname = os.environ["HOSTNAME"]
|
||||||
if '-' in hostname:
|
if "-" in hostname:
|
||||||
hostname = hostname.split('-')[-1]
|
hostname = hostname.split("-")[-1]
|
||||||
|
|
||||||
oldNetIO = dict()
|
oldNetIO = dict()
|
||||||
oldTime = time()
|
oldTime = time()
|
||||||
|
|
||||||
|
|
||||||
def update():
|
def update():
|
||||||
activeOutputs = sorted(sorted(list(filter(lambda o: o.active, outputs)), key=lambda o: o.rect.y), key=lambda o: o.rect.x)
|
activeOutputs = sorted(
|
||||||
z = ''
|
sorted(list(filter(lambda o: o.active, outputs)), key=lambda o: o.rect.y),
|
||||||
|
key=lambda o: o.rect.x,
|
||||||
|
)
|
||||||
|
z = ""
|
||||||
for aOutput in range(len(activeOutputs)):
|
for aOutput in range(len(activeOutputs)):
|
||||||
output = activeOutputs[aOutput]
|
output = activeOutputs[aOutput]
|
||||||
# Mode || Workspaces
|
# Mode || Workspaces
|
||||||
t = []
|
t = []
|
||||||
if (mode != ''):
|
if mode != "":
|
||||||
t.append(mode)
|
t.append(mode)
|
||||||
else:
|
else:
|
||||||
t.append(' '.join([(w.name.upper() if w.focused else w.name) for w in workspaces if w.output == output.name]))
|
t.append(
|
||||||
|
" ".join(
|
||||||
|
[
|
||||||
|
(w.name.upper() if w.focused else w.name)
|
||||||
|
for w in workspaces
|
||||||
|
if w.output == output.name
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Windows Title
|
# Windows Title
|
||||||
#if container:
|
# if container:
|
||||||
# t.append(container.name)
|
# t.append(container.name)
|
||||||
|
|
||||||
# CPU
|
# CPU
|
||||||
t.append('C' + ''.join([upChart(p/100) for p in psutil.cpu_percent(percpu=True)]))
|
t.append(
|
||||||
|
"C" + "".join([upChart(p / 100) for p in psutil.cpu_percent(percpu=True)])
|
||||||
|
)
|
||||||
|
|
||||||
# Memory
|
# Memory
|
||||||
t.append('M' + str(round(psutil.virtual_memory().percent)) + '% ' +
|
t.append(
|
||||||
'S' + str(round(psutil.swap_memory().percent)) + '%')
|
"M"
|
||||||
|
+ str(round(psutil.virtual_memory().percent))
|
||||||
|
+ "% "
|
||||||
|
+ "S"
|
||||||
|
+ str(round(psutil.swap_memory().percent))
|
||||||
|
+ "%"
|
||||||
|
)
|
||||||
|
|
||||||
# Disks
|
# Disks
|
||||||
d = []
|
d = []
|
||||||
for disk in psutil.disk_partitions():
|
for disk in psutil.disk_partitions():
|
||||||
e = ''
|
e = ""
|
||||||
if disk.device.startswith('/dev/sd'):
|
if disk.device.startswith("/dev/sd"):
|
||||||
e += 'S' + disk.device[-2:].upper()
|
e += "S" + disk.device[-2:].upper()
|
||||||
elif disk.device.startswith('/dev/mmcblk'):
|
elif disk.device.startswith("/dev/mmcblk"):
|
||||||
e += 'M' + disk.device[-3] + disk.device[-1]
|
e += "M" + disk.device[-3] + disk.device[-1]
|
||||||
else:
|
else:
|
||||||
e += '?'
|
e += "?"
|
||||||
e += ' '
|
e += " "
|
||||||
e += str(round(psutil.disk_usage(disk.mountpoint).percent)) + '%'
|
e += str(round(psutil.disk_usage(disk.mountpoint).percent)) + "%"
|
||||||
d.append(e)
|
d.append(e)
|
||||||
t.append(' '.join(d))
|
t.append(" ".join(d))
|
||||||
|
|
||||||
# Network
|
# Network
|
||||||
netStats = psutil.net_if_stats()
|
netStats = psutil.net_if_stats()
|
||||||
netIO = psutil.net_io_counters(pernic=True)
|
netIO = psutil.net_io_counters(pernic=True)
|
||||||
net = []
|
net = []
|
||||||
for iface in filter(lambda i: i != 'lo' and netStats[i].isup, netStats.keys()):
|
for iface in filter(lambda i: i != "lo" and netStats[i].isup, netStats.keys()):
|
||||||
s = ''
|
s = ""
|
||||||
if iface.startswith('eth'):
|
if iface.startswith("eth"):
|
||||||
s += 'E'
|
s += "E"
|
||||||
elif iface.startswith('wlan'):
|
elif iface.startswith("wlan"):
|
||||||
s += 'W'
|
s += "W"
|
||||||
else:
|
else:
|
||||||
s += '?'
|
s += "?"
|
||||||
|
|
||||||
s += ' '
|
s += " "
|
||||||
now = time()
|
now = time()
|
||||||
global oldNetIO, oldTime
|
global oldNetIO, oldTime
|
||||||
|
|
||||||
sent = ((oldNetIO[iface].bytes_sent if iface in oldNetIO else 0) - (netIO[iface].bytes_sent if iface in netIO else 0)) / (oldTime - now)
|
sent = (
|
||||||
recv = ((oldNetIO[iface].bytes_recv if iface in oldNetIO else 0) - (netIO[iface].bytes_recv if iface in netIO else 0)) / (oldTime - now)
|
(oldNetIO[iface].bytes_sent if iface in oldNetIO else 0)
|
||||||
s += '↓' + humanSizeOf(abs(recv), 'B/s') + ' ↑' + humanSizeOf(abs(sent), 'B/s')
|
- (netIO[iface].bytes_sent if iface in netIO else 0)
|
||||||
|
) / (oldTime - now)
|
||||||
|
recv = (
|
||||||
|
(oldNetIO[iface].bytes_recv if iface in oldNetIO else 0)
|
||||||
|
- (netIO[iface].bytes_recv if iface in netIO else 0)
|
||||||
|
) / (oldTime - now)
|
||||||
|
s += (
|
||||||
|
"↓"
|
||||||
|
+ humanSizeOf(abs(recv), "B/s")
|
||||||
|
+ " ↑"
|
||||||
|
+ humanSizeOf(abs(sent), "B/s")
|
||||||
|
)
|
||||||
|
|
||||||
oldNetIO = netIO
|
oldNetIO = netIO
|
||||||
oldTime = now
|
oldTime = now
|
||||||
|
|
||||||
net.append(s)
|
net.append(s)
|
||||||
t.append(' '.join(net))
|
t.append(" ".join(net))
|
||||||
|
|
||||||
# Battery
|
# Battery
|
||||||
if os.path.isdir('/sys/class/power_supply/BAT0'):
|
if os.path.isdir("/sys/class/power_supply/BAT0"):
|
||||||
with open('/sys/class/power_supply/BAT0/charge_now') as f:
|
with open("/sys/class/power_supply/BAT0/charge_now") as f:
|
||||||
charge_now = int(f.read())
|
charge_now = int(f.read())
|
||||||
with open('/sys/class/power_supply/BAT0/charge_full_design') as f:
|
with open("/sys/class/power_supply/BAT0/charge_full_design") as f:
|
||||||
charge_full = int(f.read())
|
charge_full = int(f.read())
|
||||||
t.append('B' + str(round(100*charge_now/charge_full)) + '%')
|
t.append("B" + str(round(100 * charge_now / charge_full)) + "%")
|
||||||
|
|
||||||
# Volume
|
# Volume
|
||||||
# t.append('V ' + str(alsaaudio.Mixer('Master').getvolume()[0]) + '%')
|
# t.append('V ' + str(alsaaudio.Mixer('Master').getvolume()[0]) + '%')
|
||||||
|
|
||||||
t.append(username + '@' + hostname)
|
t.append(username + "@" + hostname)
|
||||||
|
|
||||||
# print(' - '.join(t))
|
# print(' - '.join(t))
|
||||||
# t = [output.name]
|
# t = [output.name]
|
||||||
|
|
||||||
z += ' - '.join(t) + '%{S' + str(aOutput + 1) + '}'
|
z += " - ".join(t) + "%{S" + str(aOutput + 1) + "}"
|
||||||
#lemonbar.stdin.write(bytes(' - '.join(t), 'utf-8'))
|
# lemonbar.stdin.write(bytes(' - '.join(t), 'utf-8'))
|
||||||
#lemonbar.stdin.write(bytes('%{S' + str(aOutput + 1) + '}', 'utf-8'))
|
# lemonbar.stdin.write(bytes('%{S' + str(aOutput + 1) + '}', 'utf-8'))
|
||||||
|
|
||||||
lemonbar.stdin.write(bytes(z+'\n', 'utf-8'))
|
lemonbar.stdin.write(bytes(z + "\n", "utf-8"))
|
||||||
lemonbar.stdin.flush()
|
lemonbar.stdin.flush()
|
||||||
|
|
||||||
|
|
||||||
# Event listeners
|
# Event listeners
|
||||||
def on_mode(i3, e):
|
def on_mode(i3, e):
|
||||||
global mode
|
global mode
|
||||||
if (e.change == 'default'):
|
if e.change == "default":
|
||||||
mode = ''
|
mode = ""
|
||||||
else :
|
else:
|
||||||
mode = e.change
|
mode = e.change
|
||||||
update()
|
update()
|
||||||
|
|
||||||
|
|
||||||
i3.on("mode", on_mode)
|
i3.on("mode", on_mode)
|
||||||
|
|
||||||
#def on_window_focus(i3, e):
|
# def on_window_focus(i3, e):
|
||||||
# global container
|
# global container
|
||||||
# container = e.container
|
# container = e.container
|
||||||
# update()
|
# update()
|
||||||
#
|
#
|
||||||
#i3.on("window::focus", on_window_focus)
|
# i3.on("window::focus", on_window_focus)
|
||||||
|
|
||||||
|
|
||||||
def on_workspace_focus(i3, e):
|
def on_workspace_focus(i3, e):
|
||||||
global workspaces
|
global workspaces
|
||||||
workspaces = i3.get_workspaces()
|
workspaces = i3.get_workspaces()
|
||||||
update()
|
update()
|
||||||
|
|
||||||
|
|
||||||
i3.on("workspace::focus", on_workspace_focus)
|
i3.on("workspace::focus", on_workspace_focus)
|
||||||
|
|
||||||
# Starting
|
# Starting
|
||||||
|
|
|
@ -16,22 +16,37 @@ import difflib
|
||||||
FONT = "DejaVu Sans Mono for Powerline"
|
FONT = "DejaVu Sans Mono for Powerline"
|
||||||
|
|
||||||
# TODO Update to be in sync with base16
|
# TODO Update to be in sync with base16
|
||||||
thm = ['#002b36', '#dc322f', '#859900', '#b58900', '#268bd2', '#6c71c4',
|
thm = [
|
||||||
'#2aa198', '#93a1a1', '#657b83', '#dc322f', '#859900', '#b58900',
|
"#002b36",
|
||||||
'#268bd2', '#6c71c4', '#2aa198', '#fdf6e3']
|
"#dc322f",
|
||||||
fg = '#93a1a1'
|
"#859900",
|
||||||
bg = '#002b36'
|
"#b58900",
|
||||||
|
"#268bd2",
|
||||||
|
"#6c71c4",
|
||||||
|
"#2aa198",
|
||||||
|
"#93a1a1",
|
||||||
|
"#657b83",
|
||||||
|
"#dc322f",
|
||||||
|
"#859900",
|
||||||
|
"#b58900",
|
||||||
|
"#268bd2",
|
||||||
|
"#6c71c4",
|
||||||
|
"#2aa198",
|
||||||
|
"#fdf6e3",
|
||||||
|
]
|
||||||
|
fg = "#93a1a1"
|
||||||
|
bg = "#002b36"
|
||||||
|
|
||||||
THEMES = {
|
THEMES = {
|
||||||
'CENTER': (fg, bg),
|
"CENTER": (fg, bg),
|
||||||
'DEFAULT': (thm[0], thm[8]),
|
"DEFAULT": (thm[0], thm[8]),
|
||||||
'1': (thm[0], thm[9]),
|
"1": (thm[0], thm[9]),
|
||||||
'2': (thm[0], thm[10]),
|
"2": (thm[0], thm[10]),
|
||||||
'3': (thm[0], thm[11]),
|
"3": (thm[0], thm[11]),
|
||||||
'4': (thm[0], thm[12]),
|
"4": (thm[0], thm[12]),
|
||||||
'5': (thm[0], thm[13]),
|
"5": (thm[0], thm[13]),
|
||||||
'6': (thm[0], thm[14]),
|
"6": (thm[0], thm[14]),
|
||||||
'7': (thm[0], thm[15]),
|
"7": (thm[0], thm[15]),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Utils
|
# Utils
|
||||||
|
@ -49,7 +64,7 @@ def fitText(text, size):
|
||||||
diff = size - t
|
diff = size - t
|
||||||
return text + " " * diff
|
return text + " " * diff
|
||||||
else:
|
else:
|
||||||
return ''
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def fgColor(theme):
|
def fgColor(theme):
|
||||||
|
@ -63,20 +78,20 @@ def bgColor(theme):
|
||||||
|
|
||||||
|
|
||||||
class Section:
|
class Section:
|
||||||
def __init__(self, theme='DEFAULT'):
|
def __init__(self, theme="DEFAULT"):
|
||||||
self.text = ''
|
self.text = ""
|
||||||
self.size = 0
|
self.size = 0
|
||||||
self.toSize = 0
|
self.toSize = 0
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.visible = False
|
self.visible = False
|
||||||
self.name = ''
|
self.name = ""
|
||||||
|
|
||||||
def update(self, text):
|
def update(self, text):
|
||||||
if text == '':
|
if text == "":
|
||||||
self.toSize = 0
|
self.toSize = 0
|
||||||
else:
|
else:
|
||||||
if len(text) < len(self.text):
|
if len(text) < len(self.text):
|
||||||
self.text = text + self.text[len(text):]
|
self.text = text + self.text[len(text) :]
|
||||||
else:
|
else:
|
||||||
self.text = text
|
self.text = text
|
||||||
self.toSize = len(text) + 3
|
self.toSize = len(text) + 3
|
||||||
|
@ -93,39 +108,39 @@ class Section:
|
||||||
self.visible = self.size
|
self.visible = self.size
|
||||||
return self.toSize == self.size
|
return self.toSize == self.size
|
||||||
|
|
||||||
def draw(self, left=True, nextTheme='DEFAULT'):
|
def draw(self, left=True, nextTheme="DEFAULT"):
|
||||||
s = ''
|
s = ""
|
||||||
if self.visible:
|
if self.visible:
|
||||||
if not left:
|
if not left:
|
||||||
if self.theme == nextTheme:
|
if self.theme == nextTheme:
|
||||||
s += ''
|
s += ""
|
||||||
else:
|
else:
|
||||||
s += '%{F' + bgColor(self.theme) + '}'
|
s += "%{F" + bgColor(self.theme) + "}"
|
||||||
s += '%{B' + bgColor(nextTheme) + '}'
|
s += "%{B" + bgColor(nextTheme) + "}"
|
||||||
s += ''
|
s += ""
|
||||||
s += '%{F' + fgColor(self.theme) + '}'
|
s += "%{F" + fgColor(self.theme) + "}"
|
||||||
s += '%{B' + bgColor(self.theme) + '}'
|
s += "%{B" + bgColor(self.theme) + "}"
|
||||||
s += ' ' if self.size > 1 else ''
|
s += " " if self.size > 1 else ""
|
||||||
s += fitText(self.text, self.size - 3)
|
s += fitText(self.text, self.size - 3)
|
||||||
s += ' ' if self.size > 2 else ''
|
s += " " if self.size > 2 else ""
|
||||||
if left:
|
if left:
|
||||||
if self.theme == nextTheme:
|
if self.theme == nextTheme:
|
||||||
s += ''
|
s += ""
|
||||||
else:
|
else:
|
||||||
s += '%{F' + bgColor(self.theme) + '}'
|
s += "%{F" + bgColor(self.theme) + "}"
|
||||||
s += '%{B' + bgColor(nextTheme) + '}'
|
s += "%{B" + bgColor(nextTheme) + "}"
|
||||||
s += ''
|
s += ""
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
# Section definition
|
# Section definition
|
||||||
sTime = Section('3')
|
sTime = Section("3")
|
||||||
|
|
||||||
hostname = os.environ['HOSTNAME'].split('.')[0]
|
hostname = os.environ["HOSTNAME"].split(".")[0]
|
||||||
sHost = Section('2')
|
sHost = Section("2")
|
||||||
sHost.update(
|
sHost.update(
|
||||||
os.environ['USER'] + '@' + hostname.split('-')[-1]
|
os.environ["USER"] + "@" + hostname.split("-")[-1] if "-" in hostname else hostname
|
||||||
if '-' in hostname else hostname)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Groups definition
|
# Groups definition
|
||||||
|
@ -133,7 +148,7 @@ gLeft = []
|
||||||
gRight = [sTime, sHost]
|
gRight = [sTime, sHost]
|
||||||
|
|
||||||
# Bar handling
|
# Bar handling
|
||||||
bar = subprocess.Popen(['lemonbar', '-f', FONT, '-b'], stdin=subprocess.PIPE)
|
bar = subprocess.Popen(["lemonbar", "-f", FONT, "-b"], stdin=subprocess.PIPE)
|
||||||
|
|
||||||
|
|
||||||
def updateBar():
|
def updateBar():
|
||||||
|
@ -141,35 +156,45 @@ def updateBar():
|
||||||
global gLeft, gRight
|
global gLeft, gRight
|
||||||
global outputs
|
global outputs
|
||||||
|
|
||||||
text = ''
|
text = ""
|
||||||
for oi in range(len(outputs)):
|
for oi in range(len(outputs)):
|
||||||
output = outputs[oi]
|
output = outputs[oi]
|
||||||
gLeftFiltered = list(
|
gLeftFiltered = list(
|
||||||
filter(
|
filter(
|
||||||
lambda s: s.visible and (
|
lambda s: s.visible and (not s.output or s.output == output.name), gLeft
|
||||||
not s.output or s.output == output.name),
|
)
|
||||||
gLeft))
|
)
|
||||||
tLeft = ''
|
tLeft = ""
|
||||||
l = len(gLeftFiltered)
|
l = len(gLeftFiltered)
|
||||||
for gi in range(l):
|
for gi in range(l):
|
||||||
g = gLeftFiltered[gi]
|
g = gLeftFiltered[gi]
|
||||||
# Next visible section for transition
|
# Next visible section for transition
|
||||||
nextTheme = gLeftFiltered[gi + 1].theme if gi + 1 < l else 'CENTER'
|
nextTheme = gLeftFiltered[gi + 1].theme if gi + 1 < l else "CENTER"
|
||||||
tLeft = tLeft + g.draw(True, nextTheme)
|
tLeft = tLeft + g.draw(True, nextTheme)
|
||||||
|
|
||||||
tRight = ''
|
tRight = ""
|
||||||
for gi in range(len(gRight)):
|
for gi in range(len(gRight)):
|
||||||
g = gRight[gi]
|
g = gRight[gi]
|
||||||
nextTheme = 'CENTER'
|
nextTheme = "CENTER"
|
||||||
for gn in gRight[gi + 1:]:
|
for gn in gRight[gi + 1 :]:
|
||||||
if gn.visible:
|
if gn.visible:
|
||||||
nextTheme = gn.theme
|
nextTheme = gn.theme
|
||||||
break
|
break
|
||||||
tRight = g.draw(False, nextTheme) + tRight
|
tRight = g.draw(False, nextTheme) + tRight
|
||||||
text += '%{l}' + tLeft + '%{r}' + tRight + \
|
text += (
|
||||||
'%{B' + bgColor('CENTER') + '}' + '%{S' + str(oi + 1) + '}'
|
"%{l}"
|
||||||
|
+ tLeft
|
||||||
|
+ "%{r}"
|
||||||
|
+ tRight
|
||||||
|
+ "%{B"
|
||||||
|
+ bgColor("CENTER")
|
||||||
|
+ "}"
|
||||||
|
+ "%{S"
|
||||||
|
+ str(oi + 1)
|
||||||
|
+ "}"
|
||||||
|
)
|
||||||
|
|
||||||
bar.stdin.write(bytes(text + '\n', 'utf-8'))
|
bar.stdin.write(bytes(text + "\n", "utf-8"))
|
||||||
bar.stdin.flush()
|
bar.stdin.flush()
|
||||||
|
|
||||||
|
|
||||||
|
@ -182,12 +207,10 @@ def on_output():
|
||||||
global outputs
|
global outputs
|
||||||
outputs = sorted(
|
outputs = sorted(
|
||||||
sorted(
|
sorted(
|
||||||
list(
|
list(filter(lambda o: o.active, i3.get_outputs())), key=lambda o: o.rect.y
|
||||||
filter(
|
),
|
||||||
lambda o: o.active,
|
key=lambda o: o.rect.x,
|
||||||
i3.get_outputs())),
|
)
|
||||||
key=lambda o: o.rect.y),
|
|
||||||
key=lambda o: o.rect.x)
|
|
||||||
|
|
||||||
|
|
||||||
on_output()
|
on_output()
|
||||||
|
@ -209,34 +232,33 @@ def on_workspace_focus():
|
||||||
if workspace.visible:
|
if workspace.visible:
|
||||||
section.update(workspace.name)
|
section.update(workspace.name)
|
||||||
else:
|
else:
|
||||||
section.update(workspace.name.split(' ')[0])
|
section.update(workspace.name.split(" ")[0])
|
||||||
|
|
||||||
if workspace.focused:
|
if workspace.focused:
|
||||||
section.theme = '4'
|
section.theme = "4"
|
||||||
elif workspace.urgent:
|
elif workspace.urgent:
|
||||||
section.theme = '1'
|
section.theme = "1"
|
||||||
else:
|
else:
|
||||||
section.theme = '6'
|
section.theme = "6"
|
||||||
else:
|
else:
|
||||||
section.update('')
|
section.update("")
|
||||||
section.theme = '6'
|
section.theme = "6"
|
||||||
|
|
||||||
for tag, i, j, k, l in difflib.SequenceMatcher(
|
for tag, i, j, k, l in difflib.SequenceMatcher(None, sNames, wNames).get_opcodes():
|
||||||
None, sNames, wNames).get_opcodes():
|
if tag == "equal": # If the workspaces didn't changed
|
||||||
if tag == 'equal': # If the workspaces didn't changed
|
|
||||||
for a in range(j - i):
|
for a in range(j - i):
|
||||||
workspace = workspaces[k + a]
|
workspace = workspaces[k + a]
|
||||||
section = gLeft[i + a]
|
section = gLeft[i + a]
|
||||||
actuate(section, workspace)
|
actuate(section, workspace)
|
||||||
newGLeft.append(section)
|
newGLeft.append(section)
|
||||||
if tag in ('delete', 'replace'): # If the workspaces were removed
|
if tag in ("delete", "replace"): # If the workspaces were removed
|
||||||
for section in gLeft[i:j]:
|
for section in gLeft[i:j]:
|
||||||
if section.visible:
|
if section.visible:
|
||||||
actuate(section, None)
|
actuate(section, None)
|
||||||
newGLeft.append(section)
|
newGLeft.append(section)
|
||||||
else:
|
else:
|
||||||
del section
|
del section
|
||||||
if tag in ('insert', 'replace'): # If the workspaces were removed
|
if tag in ("insert", "replace"): # If the workspaces were removed
|
||||||
for workspace in workspaces[k:l]:
|
for workspace in workspaces[k:l]:
|
||||||
section = Section()
|
section = Section()
|
||||||
actuate(section, workspace)
|
actuate(section, workspace)
|
||||||
|
@ -255,12 +277,14 @@ def i3events(i3childPipe):
|
||||||
# Proxy functions
|
# Proxy functions
|
||||||
def on_workspace_focus(i3, e):
|
def on_workspace_focus(i3, e):
|
||||||
global i3childPipe
|
global i3childPipe
|
||||||
i3childPipe.send('on_workspace_focus')
|
i3childPipe.send("on_workspace_focus")
|
||||||
|
|
||||||
i3.on("workspace::focus", on_workspace_focus)
|
i3.on("workspace::focus", on_workspace_focus)
|
||||||
|
|
||||||
def on_output(i3, e):
|
def on_output(i3, e):
|
||||||
global i3childPipe
|
global i3childPipe
|
||||||
i3childPipe.send('on_output')
|
i3childPipe.send("on_output")
|
||||||
|
|
||||||
i3.on("output", on_output)
|
i3.on("output", on_output)
|
||||||
|
|
||||||
i3.main()
|
i3.main()
|
||||||
|
@ -274,7 +298,7 @@ i3process.start()
|
||||||
def updateValues():
|
def updateValues():
|
||||||
# Time
|
# Time
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
sTime.update(now.strftime('%x %X'))
|
sTime.update(now.strftime("%x %X"))
|
||||||
|
|
||||||
|
|
||||||
def updateAnimation():
|
def updateAnimation():
|
||||||
|
@ -288,9 +312,9 @@ while True:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if i3parentPipe.poll():
|
if i3parentPipe.poll():
|
||||||
msg = i3parentPipe.recv()
|
msg = i3parentPipe.recv()
|
||||||
if msg == 'on_workspace_focus':
|
if msg == "on_workspace_focus":
|
||||||
on_workspace_focus()
|
on_workspace_focus()
|
||||||
elif msg == 'on_output':
|
elif msg == "on_output":
|
||||||
on_output()
|
on_output()
|
||||||
# TODO Restart lemonbar
|
# TODO Restart lemonbar
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -16,17 +16,18 @@ import mpd
|
||||||
import random
|
import random
|
||||||
import math
|
import math
|
||||||
|
|
||||||
coloredlogs.install(level='DEBUG', fmt='%(levelname)s %(message)s')
|
coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s")
|
||||||
log = logging.getLogger()
|
log = logging.getLogger()
|
||||||
|
|
||||||
# TODO Generator class (for I3WorkspacesProvider, NetworkProvider and later
|
# TODO Generator class (for I3WorkspacesProvider, NetworkProvider and later
|
||||||
# PulseaudioProvider and MpdProvider)
|
# PulseaudioProvider and MpdProvider)
|
||||||
|
|
||||||
|
|
||||||
def humanSize(num):
|
def humanSize(num):
|
||||||
"""
|
"""
|
||||||
Returns a string of width 3+3
|
Returns a string of width 3+3
|
||||||
"""
|
"""
|
||||||
for unit in ('B ','KiB','MiB','GiB','TiB','PiB','EiB','ZiB'):
|
for unit in ("B ", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB"):
|
||||||
if abs(num) < 1000:
|
if abs(num) < 1000:
|
||||||
if num >= 10:
|
if num >= 10:
|
||||||
return "{:3d}{}".format(int(num), unit)
|
return "{:3d}{}".format(int(num), unit)
|
||||||
|
@ -35,16 +36,15 @@ def humanSize(num):
|
||||||
num /= 1024.0
|
num /= 1024.0
|
||||||
return "{:d}YiB".format(num)
|
return "{:d}YiB".format(num)
|
||||||
|
|
||||||
|
|
||||||
def randomColor(seed=0):
|
def randomColor(seed=0):
|
||||||
random.seed(seed)
|
random.seed(seed)
|
||||||
return '#{:02x}{:02x}{:02x}'.format(*[random.randint(0, 255) for _ in range(3)])
|
return "#{:02x}{:02x}{:02x}".format(*[random.randint(0, 255) for _ in range(3)])
|
||||||
|
|
||||||
|
|
||||||
class TimeProvider(StatefulSection, PeriodicUpdater):
|
class TimeProvider(StatefulSection, PeriodicUpdater):
|
||||||
|
|
||||||
FORMATS = ["%H:%M",
|
FORMATS = ["%H:%M", "%m-%d %H:%M:%S", "%a %y-%m-%d %H:%M:%S"]
|
||||||
"%m-%d %H:%M:%S",
|
|
||||||
"%a %y-%m-%d %H:%M:%S"]
|
|
||||||
NUMBER_STATES = len(FORMATS)
|
NUMBER_STATES = len(FORMATS)
|
||||||
DEFAULT_STATE = 1
|
DEFAULT_STATE = 1
|
||||||
|
|
||||||
|
@ -55,18 +55,18 @@ class TimeProvider(StatefulSection, PeriodicUpdater):
|
||||||
def __init__(self, theme=None):
|
def __init__(self, theme=None):
|
||||||
PeriodicUpdater.__init__(self)
|
PeriodicUpdater.__init__(self)
|
||||||
StatefulSection.__init__(self, theme)
|
StatefulSection.__init__(self, theme)
|
||||||
self.changeInterval(1) # TODO OPTI When state < 1
|
self.changeInterval(1) # TODO OPTI When state < 1
|
||||||
|
|
||||||
|
|
||||||
class AlertLevel(enum.Enum):
|
class AlertLevel(enum.Enum):
|
||||||
NORMAL = 0
|
NORMAL = 0
|
||||||
WARNING = 1
|
WARNING = 1
|
||||||
DANGER = 2
|
DANGER = 2
|
||||||
|
|
||||||
|
|
||||||
class AlertingSection(StatefulSection):
|
class AlertingSection(StatefulSection):
|
||||||
# TODO EASE Correct settings for themes
|
# TODO EASE Correct settings for themes
|
||||||
THEMES = {AlertLevel.NORMAL: 2,
|
THEMES = {AlertLevel.NORMAL: 2, AlertLevel.WARNING: 3, AlertLevel.DANGER: 1}
|
||||||
AlertLevel.WARNING: 3,
|
|
||||||
AlertLevel.DANGER: 1}
|
|
||||||
PERSISTENT = True
|
PERSISTENT = True
|
||||||
|
|
||||||
def getLevel(self, quantity):
|
def getLevel(self, quantity):
|
||||||
|
@ -92,16 +92,16 @@ class AlertingSection(StatefulSection):
|
||||||
|
|
||||||
class CpuProvider(AlertingSection, PeriodicUpdater):
|
class CpuProvider(AlertingSection, PeriodicUpdater):
|
||||||
NUMBER_STATES = 3
|
NUMBER_STATES = 3
|
||||||
ICON = ''
|
ICON = ""
|
||||||
|
|
||||||
def fetcher(self):
|
def fetcher(self):
|
||||||
percent = psutil.cpu_percent(percpu=False)
|
percent = psutil.cpu_percent(percpu=False)
|
||||||
self.updateLevel(percent/100)
|
self.updateLevel(percent / 100)
|
||||||
if self.state >= 2:
|
if self.state >= 2:
|
||||||
percents = psutil.cpu_percent(percpu=True)
|
percents = psutil.cpu_percent(percpu=True)
|
||||||
return ''.join([Section.ramp(p/100) for p in percents])
|
return "".join([Section.ramp(p / 100) for p in percents])
|
||||||
elif self.state >= 1:
|
elif self.state >= 1:
|
||||||
return Section.ramp(percent/100)
|
return Section.ramp(percent / 100)
|
||||||
|
|
||||||
def __init__(self, theme=None):
|
def __init__(self, theme=None):
|
||||||
AlertingSection.__init__(self, theme)
|
AlertingSection.__init__(self, theme)
|
||||||
|
@ -113,12 +113,13 @@ class RamProvider(AlertingSection, PeriodicUpdater):
|
||||||
"""
|
"""
|
||||||
Shows free RAM
|
Shows free RAM
|
||||||
"""
|
"""
|
||||||
|
|
||||||
NUMBER_STATES = 4
|
NUMBER_STATES = 4
|
||||||
ICON = ''
|
ICON = ""
|
||||||
|
|
||||||
def fetcher(self):
|
def fetcher(self):
|
||||||
mem = psutil.virtual_memory()
|
mem = psutil.virtual_memory()
|
||||||
freePerc = mem.percent/100
|
freePerc = mem.percent / 100
|
||||||
self.updateLevel(freePerc)
|
self.updateLevel(freePerc)
|
||||||
|
|
||||||
if self.state < 1:
|
if self.state < 1:
|
||||||
|
@ -130,7 +131,7 @@ class RamProvider(AlertingSection, PeriodicUpdater):
|
||||||
text.append(freeStr)
|
text.append(freeStr)
|
||||||
if self.state >= 3:
|
if self.state >= 3:
|
||||||
totalStr = humanSize(mem.total)
|
totalStr = humanSize(mem.total)
|
||||||
text.append('/', totalStr)
|
text.append("/", totalStr)
|
||||||
|
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
@ -146,18 +147,18 @@ class TemperatureProvider(AlertingSection, PeriodicUpdater):
|
||||||
|
|
||||||
def fetcher(self):
|
def fetcher(self):
|
||||||
allTemp = psutil.sensors_temperatures()
|
allTemp = psutil.sensors_temperatures()
|
||||||
if 'coretemp' not in allTemp:
|
if "coretemp" not in allTemp:
|
||||||
# TODO Opti Remove interval
|
# TODO Opti Remove interval
|
||||||
return ''
|
return ""
|
||||||
temp = allTemp['coretemp'][0]
|
temp = allTemp["coretemp"][0]
|
||||||
|
|
||||||
self.warningThresold = temp.high
|
self.warningThresold = temp.high
|
||||||
self.dangerThresold = temp.critical
|
self.dangerThresold = temp.critical
|
||||||
self.updateLevel(temp.current)
|
self.updateLevel(temp.current)
|
||||||
|
|
||||||
self.icon = Section.ramp(temp.current/temp.high, self.RAMP)
|
self.icon = Section.ramp(temp.current / temp.high, self.RAMP)
|
||||||
if self.state >= 1:
|
if self.state >= 1:
|
||||||
return '{:.0f}°C'.format(temp.current)
|
return "{:.0f}°C".format(temp.current)
|
||||||
|
|
||||||
def __init__(self, theme=None):
|
def __init__(self, theme=None):
|
||||||
AlertingSection.__init__(self, theme)
|
AlertingSection.__init__(self, theme)
|
||||||
|
@ -176,15 +177,16 @@ class BatteryProvider(AlertingSection, PeriodicUpdater):
|
||||||
self.icon = None
|
self.icon = None
|
||||||
return None
|
return None
|
||||||
|
|
||||||
self.icon = ("" if bat.power_plugged else "") + \
|
self.icon = ("" if bat.power_plugged else "") + Section.ramp(
|
||||||
Section.ramp(bat.percent/100, self.RAMP)
|
bat.percent / 100, self.RAMP
|
||||||
|
)
|
||||||
|
|
||||||
self.updateLevel(1-bat.percent/100)
|
self.updateLevel(1 - bat.percent / 100)
|
||||||
|
|
||||||
if self.state < 1:
|
if self.state < 1:
|
||||||
return
|
return
|
||||||
|
|
||||||
t = Text('{:.0f}%'.format(bat.percent))
|
t = Text("{:.0f}%".format(bat.percent))
|
||||||
|
|
||||||
if self.state < 2:
|
if self.state < 2:
|
||||||
return t
|
return t
|
||||||
|
@ -200,7 +202,6 @@ class BatteryProvider(AlertingSection, PeriodicUpdater):
|
||||||
self.changeInterval(5)
|
self.changeInterval(5)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PulseaudioProvider(StatefulSection, ThreadedUpdater):
|
class PulseaudioProvider(StatefulSection, ThreadedUpdater):
|
||||||
NUMBER_STATES = 3
|
NUMBER_STATES = 3
|
||||||
DEFAULT_STATE = 1
|
DEFAULT_STATE = 1
|
||||||
|
@ -208,28 +209,27 @@ class PulseaudioProvider(StatefulSection, ThreadedUpdater):
|
||||||
def __init__(self, theme=None):
|
def __init__(self, theme=None):
|
||||||
ThreadedUpdater.__init__(self)
|
ThreadedUpdater.__init__(self)
|
||||||
StatefulSection.__init__(self, theme)
|
StatefulSection.__init__(self, theme)
|
||||||
self.pulseEvents = pulsectl.Pulse('event-handler')
|
self.pulseEvents = pulsectl.Pulse("event-handler")
|
||||||
|
|
||||||
self.pulseEvents.event_mask_set(pulsectl.PulseEventMaskEnum.sink)
|
self.pulseEvents.event_mask_set(pulsectl.PulseEventMaskEnum.sink)
|
||||||
self.pulseEvents.event_callback_set(self.handleEvent)
|
self.pulseEvents.event_callback_set(self.handleEvent)
|
||||||
self.start()
|
self.start()
|
||||||
self.refreshData()
|
self.refreshData()
|
||||||
|
|
||||||
|
|
||||||
def fetcher(self):
|
def fetcher(self):
|
||||||
sinks = []
|
sinks = []
|
||||||
with pulsectl.Pulse('list-sinks') as pulse:
|
with pulsectl.Pulse("list-sinks") as pulse:
|
||||||
for sink in pulse.sink_list():
|
for sink in pulse.sink_list():
|
||||||
if sink.port_active.name == "analog-output-headphones":
|
if sink.port_active.name == "analog-output-headphones":
|
||||||
icon = ""
|
icon = ""
|
||||||
elif sink.port_active.name == "analog-output-speaker":
|
elif sink.port_active.name == "analog-output-speaker":
|
||||||
icon = "" if sink.mute else ""
|
icon = "" if sink.mute else ""
|
||||||
elif sink.port_active.name == "headset-output":
|
elif sink.port_active.name == "headset-output":
|
||||||
icon = ''
|
icon = ""
|
||||||
else:
|
else:
|
||||||
icon = "?"
|
icon = "?"
|
||||||
vol = pulse.volume_get_all_chans(sink)
|
vol = pulse.volume_get_all_chans(sink)
|
||||||
fg = (sink.mute and '#333333') or (vol > 1 and '#FF0000') or None
|
fg = (sink.mute and "#333333") or (vol > 1 and "#FF0000") or None
|
||||||
|
|
||||||
t = Text(icon, fg=fg)
|
t = Text(icon, fg=fg)
|
||||||
sinks.append(t)
|
sinks.append(t)
|
||||||
|
@ -245,7 +245,7 @@ class PulseaudioProvider(StatefulSection, ThreadedUpdater):
|
||||||
vol -= 1
|
vol -= 1
|
||||||
t.append(ramp)
|
t.append(ramp)
|
||||||
else:
|
else:
|
||||||
t.append(" {:2.0f}%".format(vol*100))
|
t.append(" {:2.0f}%".format(vol * 100))
|
||||||
|
|
||||||
return Text(*sinks)
|
return Text(*sinks)
|
||||||
|
|
||||||
|
@ -263,27 +263,27 @@ class NetworkProviderSection(StatefulSection, Updater):
|
||||||
|
|
||||||
def actType(self):
|
def actType(self):
|
||||||
self.ssid = None
|
self.ssid = None
|
||||||
if self.iface.startswith('eth') or self.iface.startswith('enp'):
|
if self.iface.startswith("eth") or self.iface.startswith("enp"):
|
||||||
if 'u' in self.iface:
|
if "u" in self.iface:
|
||||||
self.icon = ''
|
self.icon = ""
|
||||||
else:
|
else:
|
||||||
self.icon = ''
|
self.icon = ""
|
||||||
elif self.iface.startswith('wlan') or self.iface.startswith('wl'):
|
elif self.iface.startswith("wlan") or self.iface.startswith("wl"):
|
||||||
self.icon = ''
|
self.icon = ""
|
||||||
if self.showSsid:
|
if self.showSsid:
|
||||||
cmd = ["iwgetid", self.iface, "--raw"]
|
cmd = ["iwgetid", self.iface, "--raw"]
|
||||||
p = subprocess.run(cmd, stdout=subprocess.PIPE)
|
p = subprocess.run(cmd, stdout=subprocess.PIPE)
|
||||||
self.ssid = p.stdout.strip().decode()
|
self.ssid = p.stdout.strip().decode()
|
||||||
elif self.iface.startswith('tun') or self.iface.startswith('tap'):
|
elif self.iface.startswith("tun") or self.iface.startswith("tap"):
|
||||||
self.icon = ''
|
self.icon = ""
|
||||||
elif self.iface.startswith('docker'):
|
elif self.iface.startswith("docker"):
|
||||||
self.icon = ''
|
self.icon = ""
|
||||||
elif self.iface.startswith('veth'):
|
elif self.iface.startswith("veth"):
|
||||||
self.icon = ''
|
self.icon = ""
|
||||||
elif self.iface.startswith('vboxnet'):
|
elif self.iface.startswith("vboxnet"):
|
||||||
self.icon = ''
|
self.icon = ""
|
||||||
else:
|
else:
|
||||||
self.icon = '?'
|
self.icon = "?"
|
||||||
|
|
||||||
def getAddresses(self):
|
def getAddresses(self):
|
||||||
ipv4 = None
|
ipv4 = None
|
||||||
|
@ -298,9 +298,11 @@ class NetworkProviderSection(StatefulSection, Updater):
|
||||||
def fetcher(self):
|
def fetcher(self):
|
||||||
self.icon = None
|
self.icon = None
|
||||||
self.persistent = False
|
self.persistent = False
|
||||||
if self.iface not in self.parent.stats or \
|
if (
|
||||||
not self.parent.stats[self.iface].isup or \
|
self.iface not in self.parent.stats
|
||||||
self.iface.startswith('lo'):
|
or not self.parent.stats[self.iface].isup
|
||||||
|
or self.iface.startswith("lo")
|
||||||
|
):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Get addresses
|
# Get addresses
|
||||||
|
@ -317,30 +319,36 @@ class NetworkProviderSection(StatefulSection, Updater):
|
||||||
|
|
||||||
if self.showAddress:
|
if self.showAddress:
|
||||||
if ipv4:
|
if ipv4:
|
||||||
netStrFull = '{}/{}'.format(ipv4.address, ipv4.netmask)
|
netStrFull = "{}/{}".format(ipv4.address, ipv4.netmask)
|
||||||
addr = ipaddress.IPv4Network(netStrFull, strict=False)
|
addr = ipaddress.IPv4Network(netStrFull, strict=False)
|
||||||
addrStr = '{}/{}'.format(ipv4.address, addr.prefixlen)
|
addrStr = "{}/{}".format(ipv4.address, addr.prefixlen)
|
||||||
text.append(addrStr)
|
text.append(addrStr)
|
||||||
# TODO IPV6
|
# TODO IPV6
|
||||||
# if ipv6:
|
# if ipv6:
|
||||||
# text += ' ' + ipv6.address
|
# text += ' ' + ipv6.address
|
||||||
|
|
||||||
if self.showSpeed:
|
if self.showSpeed:
|
||||||
recvDiff = self.parent.IO[self.iface].bytes_recv \
|
recvDiff = (
|
||||||
|
self.parent.IO[self.iface].bytes_recv
|
||||||
- self.parent.prevIO[self.iface].bytes_recv
|
- self.parent.prevIO[self.iface].bytes_recv
|
||||||
sentDiff = self.parent.IO[self.iface].bytes_sent \
|
)
|
||||||
|
sentDiff = (
|
||||||
|
self.parent.IO[self.iface].bytes_sent
|
||||||
- self.parent.prevIO[self.iface].bytes_sent
|
- self.parent.prevIO[self.iface].bytes_sent
|
||||||
|
)
|
||||||
recvDiff /= self.parent.dt
|
recvDiff /= self.parent.dt
|
||||||
sentDiff /= self.parent.dt
|
sentDiff /= self.parent.dt
|
||||||
text.append('↓{}↑{}'.format(humanSize(recvDiff),
|
text.append("↓{}↑{}".format(humanSize(recvDiff), humanSize(sentDiff)))
|
||||||
humanSize(sentDiff)))
|
|
||||||
|
|
||||||
if self.showTransfer:
|
if self.showTransfer:
|
||||||
text.append('⇓{}⇑{}'.format(
|
text.append(
|
||||||
humanSize(self.parent.IO[self.iface].bytes_recv),
|
"⇓{}⇑{}".format(
|
||||||
humanSize(self.parent.IO[self.iface].bytes_sent)))
|
humanSize(self.parent.IO[self.iface].bytes_recv),
|
||||||
|
humanSize(self.parent.IO[self.iface].bytes_sent),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return ' '.join(text)
|
return " ".join(text)
|
||||||
|
|
||||||
def onChangeState(self, state):
|
def onChangeState(self, state):
|
||||||
self.showSsid = state >= 1
|
self.showSsid = state >= 1
|
||||||
|
@ -402,32 +410,33 @@ class NetworkProvider(Section, PeriodicUpdater):
|
||||||
self.fetchData()
|
self.fetchData()
|
||||||
self.changeInterval(5)
|
self.changeInterval(5)
|
||||||
|
|
||||||
|
|
||||||
class RfkillProvider(Section, PeriodicUpdater):
|
class RfkillProvider(Section, PeriodicUpdater):
|
||||||
# TODO FEAT rfkill doesn't seem to indicate that the hardware switch is
|
# TODO FEAT rfkill doesn't seem to indicate that the hardware switch is
|
||||||
# toggled
|
# toggled
|
||||||
PATH = '/sys/class/rfkill'
|
PATH = "/sys/class/rfkill"
|
||||||
|
|
||||||
def fetcher(self):
|
def fetcher(self):
|
||||||
t = Text()
|
t = Text()
|
||||||
for device in os.listdir(self.PATH):
|
for device in os.listdir(self.PATH):
|
||||||
with open(os.path.join(self.PATH, device, 'soft'), 'rb') as f:
|
with open(os.path.join(self.PATH, device, "soft"), "rb") as f:
|
||||||
softBlocked = f.read().strip() != b'0'
|
softBlocked = f.read().strip() != b"0"
|
||||||
with open(os.path.join(self.PATH, device, 'hard'), 'rb') as f:
|
with open(os.path.join(self.PATH, device, "hard"), "rb") as f:
|
||||||
hardBlocked = f.read().strip() != b'0'
|
hardBlocked = f.read().strip() != b"0"
|
||||||
|
|
||||||
if not hardBlocked and not softBlocked:
|
if not hardBlocked and not softBlocked:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
with open(os.path.join(self.PATH, device, 'type'), 'rb') as f:
|
with open(os.path.join(self.PATH, device, "type"), "rb") as f:
|
||||||
typ = f.read().strip()
|
typ = f.read().strip()
|
||||||
|
|
||||||
fg = (hardBlocked and '#CCCCCC') or (softBlocked and '#FF0000')
|
fg = (hardBlocked and "#CCCCCC") or (softBlocked and "#FF0000")
|
||||||
if typ == b'wlan':
|
if typ == b"wlan":
|
||||||
icon = ''
|
icon = ""
|
||||||
elif typ == b'bluetooth':
|
elif typ == b"bluetooth":
|
||||||
icon = ''
|
icon = ""
|
||||||
else:
|
else:
|
||||||
icon = '?'
|
icon = "?"
|
||||||
|
|
||||||
t.append(Text(icon, fg=fg))
|
t.append(Text(icon, fg=fg))
|
||||||
return t
|
return t
|
||||||
|
@ -437,6 +446,7 @@ class RfkillProvider(Section, PeriodicUpdater):
|
||||||
Section.__init__(self, theme)
|
Section.__init__(self, theme)
|
||||||
self.changeInterval(5)
|
self.changeInterval(5)
|
||||||
|
|
||||||
|
|
||||||
class SshAgentProvider(PeriodicUpdater):
|
class SshAgentProvider(PeriodicUpdater):
|
||||||
def fetcher(self):
|
def fetcher(self):
|
||||||
cmd = ["ssh-add", "-l"]
|
cmd = ["ssh-add", "-l"]
|
||||||
|
@ -444,17 +454,18 @@ class SshAgentProvider(PeriodicUpdater):
|
||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
return None
|
return None
|
||||||
text = Text()
|
text = Text()
|
||||||
for line in proc.stdout.split(b'\n'):
|
for line in proc.stdout.split(b"\n"):
|
||||||
if not len(line):
|
if not len(line):
|
||||||
continue
|
continue
|
||||||
fingerprint = line.split()[1]
|
fingerprint = line.split()[1]
|
||||||
text.append(Text('', fg=randomColor(seed=fingerprint)))
|
text.append(Text("", fg=randomColor(seed=fingerprint)))
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
PeriodicUpdater.__init__(self)
|
PeriodicUpdater.__init__(self)
|
||||||
self.changeInterval(5)
|
self.changeInterval(5)
|
||||||
|
|
||||||
|
|
||||||
class GpgAgentProvider(PeriodicUpdater):
|
class GpgAgentProvider(PeriodicUpdater):
|
||||||
def fetcher(self):
|
def fetcher(self):
|
||||||
cmd = ["gpg-connect-agent", "keyinfo --list", "/bye"]
|
cmd = ["gpg-connect-agent", "keyinfo --list", "/bye"]
|
||||||
|
@ -463,39 +474,41 @@ class GpgAgentProvider(PeriodicUpdater):
|
||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
return None
|
return None
|
||||||
text = Text()
|
text = Text()
|
||||||
for line in proc.stdout.split(b'\n'):
|
for line in proc.stdout.split(b"\n"):
|
||||||
if not len(line) or line == b'OK':
|
if not len(line) or line == b"OK":
|
||||||
continue
|
continue
|
||||||
spli = line.split()
|
spli = line.split()
|
||||||
if spli[6] != b'1':
|
if spli[6] != b"1":
|
||||||
continue
|
continue
|
||||||
keygrip = spli[2]
|
keygrip = spli[2]
|
||||||
text.append(Text('', fg=randomColor(seed=keygrip)))
|
text.append(Text("", fg=randomColor(seed=keygrip)))
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
PeriodicUpdater.__init__(self)
|
PeriodicUpdater.__init__(self)
|
||||||
self.changeInterval(5)
|
self.changeInterval(5)
|
||||||
|
|
||||||
|
|
||||||
class KeystoreProvider(Section, MergedUpdater):
|
class KeystoreProvider(Section, MergedUpdater):
|
||||||
# TODO OPTI+FEAT Use ColorCountsSection and not MergedUpdater, this is useless
|
# TODO OPTI+FEAT Use ColorCountsSection and not MergedUpdater, this is useless
|
||||||
ICON = ''
|
ICON = ""
|
||||||
|
|
||||||
def __init__(self, theme=None):
|
def __init__(self, theme=None):
|
||||||
MergedUpdater.__init__(self, SshAgentProvider(), GpgAgentProvider())
|
MergedUpdater.__init__(self, SshAgentProvider(), GpgAgentProvider())
|
||||||
Section.__init__(self, theme)
|
Section.__init__(self, theme)
|
||||||
|
|
||||||
|
|
||||||
class NotmuchUnreadProvider(ColorCountsSection, InotifyUpdater):
|
class NotmuchUnreadProvider(ColorCountsSection, InotifyUpdater):
|
||||||
COLORABLE_ICON = ''
|
COLORABLE_ICON = ""
|
||||||
|
|
||||||
def subfetcher(self):
|
def subfetcher(self):
|
||||||
db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY, path=self.dir)
|
db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY, path=self.dir)
|
||||||
counts = []
|
counts = []
|
||||||
for account in self.accounts:
|
for account in self.accounts:
|
||||||
queryStr = 'folder:/{}/ and tag:unread'.format(account)
|
queryStr = "folder:/{}/ and tag:unread".format(account)
|
||||||
query = notmuch.Query(db, queryStr)
|
query = notmuch.Query(db, queryStr)
|
||||||
nbMsgs = query.count_messages()
|
nbMsgs = query.count_messages()
|
||||||
if account == 'frogeye':
|
if account == "frogeye":
|
||||||
global q
|
global q
|
||||||
q = query
|
q = query
|
||||||
if nbMsgs < 1:
|
if nbMsgs < 1:
|
||||||
|
@ -504,7 +517,7 @@ class NotmuchUnreadProvider(ColorCountsSection, InotifyUpdater):
|
||||||
# db.close()
|
# db.close()
|
||||||
return counts
|
return counts
|
||||||
|
|
||||||
def __init__(self, dir='~/.mail/', theme=None):
|
def __init__(self, dir="~/.mail/", theme=None):
|
||||||
PeriodicUpdater.__init__(self)
|
PeriodicUpdater.__init__(self)
|
||||||
ColorCountsSection.__init__(self, theme)
|
ColorCountsSection.__init__(self, theme)
|
||||||
|
|
||||||
|
@ -512,23 +525,24 @@ class NotmuchUnreadProvider(ColorCountsSection, InotifyUpdater):
|
||||||
assert os.path.isdir(self.dir)
|
assert os.path.isdir(self.dir)
|
||||||
|
|
||||||
# Fetching account list
|
# Fetching account list
|
||||||
self.accounts = sorted([a for a in os.listdir(self.dir)
|
self.accounts = sorted(
|
||||||
if not a.startswith('.')])
|
[a for a in os.listdir(self.dir) if not a.startswith(".")]
|
||||||
|
)
|
||||||
# Fetching colors
|
# Fetching colors
|
||||||
self.colors = dict()
|
self.colors = dict()
|
||||||
for account in self.accounts:
|
for account in self.accounts:
|
||||||
filename = os.path.join(self.dir, account, 'color')
|
filename = os.path.join(self.dir, account, "color")
|
||||||
with open(filename, 'r') as f:
|
with open(filename, "r") as f:
|
||||||
color = f.read().strip()
|
color = f.read().strip()
|
||||||
self.colors[account] = color
|
self.colors[account] = color
|
||||||
|
|
||||||
self.addPath(os.path.join(self.dir, '.notmuch', 'xapian'))
|
self.addPath(os.path.join(self.dir, ".notmuch", "xapian"))
|
||||||
|
|
||||||
|
|
||||||
class TodoProvider(ColorCountsSection, InotifyUpdater):
|
class TodoProvider(ColorCountsSection, InotifyUpdater):
|
||||||
# TODO OPT/UX Maybe we could get more data from the todoman python module
|
# TODO OPT/UX Maybe we could get more data from the todoman python module
|
||||||
# TODO OPT Specific callback for specific directory
|
# TODO OPT Specific callback for specific directory
|
||||||
COLORABLE_ICON = ''
|
COLORABLE_ICON = ""
|
||||||
|
|
||||||
def updateCalendarList(self):
|
def updateCalendarList(self):
|
||||||
calendars = sorted(os.listdir(self.dir))
|
calendars = sorted(os.listdir(self.dir))
|
||||||
|
@ -538,13 +552,13 @@ class TodoProvider(ColorCountsSection, InotifyUpdater):
|
||||||
self.addPath(os.path.join(self.dir, calendar), refresh=False)
|
self.addPath(os.path.join(self.dir, calendar), refresh=False)
|
||||||
|
|
||||||
# Fetching name
|
# Fetching name
|
||||||
path = os.path.join(self.dir, calendar, 'displayname')
|
path = os.path.join(self.dir, calendar, "displayname")
|
||||||
with open(path, 'r') as f:
|
with open(path, "r") as f:
|
||||||
self.names[calendar] = f.read().strip()
|
self.names[calendar] = f.read().strip()
|
||||||
|
|
||||||
# Fetching color
|
# Fetching color
|
||||||
path = os.path.join(self.dir, calendar, 'color')
|
path = os.path.join(self.dir, calendar, "color")
|
||||||
with open(path, 'r') as f:
|
with open(path, "r") as f:
|
||||||
self.colors[calendar] = f.read().strip()
|
self.colors[calendar] = f.read().strip()
|
||||||
self.calendars = calendars
|
self.calendars = calendars
|
||||||
|
|
||||||
|
@ -579,8 +593,8 @@ class TodoProvider(ColorCountsSection, InotifyUpdater):
|
||||||
if self.state < 2:
|
if self.state < 2:
|
||||||
c = self.countUndone(None)
|
c = self.countUndone(None)
|
||||||
if c > 0:
|
if c > 0:
|
||||||
counts.append((c, '#00000'))
|
counts.append((c, "#00000"))
|
||||||
counts.append((0, '#FFFFF'))
|
counts.append((0, "#FFFFF"))
|
||||||
return counts
|
return counts
|
||||||
# Optimisation ends here
|
# Optimisation ends here
|
||||||
|
|
||||||
|
@ -591,6 +605,7 @@ class TodoProvider(ColorCountsSection, InotifyUpdater):
|
||||||
counts.append((c, self.colors[calendar]))
|
counts.append((c, self.colors[calendar]))
|
||||||
return counts
|
return counts
|
||||||
|
|
||||||
|
|
||||||
class I3WindowTitleProvider(Section, I3Updater):
|
class I3WindowTitleProvider(Section, I3Updater):
|
||||||
# TODO FEAT To make this available from start, we need to find the
|
# TODO FEAT To make this available from start, we need to find the
|
||||||
# `focused=True` element following the `focus` array
|
# `focused=True` element following the `focus` array
|
||||||
|
@ -603,6 +618,7 @@ class I3WindowTitleProvider(Section, I3Updater):
|
||||||
Section.__init__(self, theme=theme)
|
Section.__init__(self, theme=theme)
|
||||||
self.on("window", self.on_window)
|
self.on("window", self.on_window)
|
||||||
|
|
||||||
|
|
||||||
class I3WorkspacesProviderSection(Section):
|
class I3WorkspacesProviderSection(Section):
|
||||||
def selectTheme(self):
|
def selectTheme(self):
|
||||||
if self.urgent:
|
if self.urgent:
|
||||||
|
@ -626,12 +642,12 @@ class I3WorkspacesProviderSection(Section):
|
||||||
|
|
||||||
def setName(self, name):
|
def setName(self, name):
|
||||||
self.shortName = name
|
self.shortName = name
|
||||||
self.fullName = self.parent.customNames[name] \
|
self.fullName = (
|
||||||
if name in self.parent.customNames else name
|
self.parent.customNames[name] if name in self.parent.customNames else name
|
||||||
|
)
|
||||||
|
|
||||||
def switchTo(self):
|
def switchTo(self):
|
||||||
self.parent.i3.command('workspace {}'.format(self.shortName))
|
self.parent.i3.command("workspace {}".format(self.shortName))
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, name, parent):
|
def __init__(self, name, parent):
|
||||||
Section.__init__(self)
|
Section.__init__(self)
|
||||||
|
@ -652,7 +668,6 @@ class I3WorkspacesProviderSection(Section):
|
||||||
self.updateText(None)
|
self.updateText(None)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class I3WorkspacesProvider(Section, I3Updater):
|
class I3WorkspacesProvider(Section, I3Updater):
|
||||||
# TODO FEAT Multi-screen
|
# TODO FEAT Multi-screen
|
||||||
|
|
||||||
|
@ -713,7 +728,7 @@ class I3WorkspacesProvider(Section, I3Updater):
|
||||||
self.sections[e.current.num].show()
|
self.sections[e.current.num].show()
|
||||||
|
|
||||||
def on_mode(self, i3, e):
|
def on_mode(self, i3, e):
|
||||||
if e.change == 'default':
|
if e.change == "default":
|
||||||
self.modeSection.updateText(None)
|
self.modeSection.updateText(None)
|
||||||
for section in self.sections.values():
|
for section in self.sections.values():
|
||||||
section.tempShow()
|
section.tempShow()
|
||||||
|
@ -722,7 +737,9 @@ class I3WorkspacesProvider(Section, I3Updater):
|
||||||
for section in self.sections.values():
|
for section in self.sections.values():
|
||||||
section.tempEmpty()
|
section.tempEmpty()
|
||||||
|
|
||||||
def __init__(self, theme=0, themeFocus=3, themeUrgent=1, themeMode=2, customNames=dict()):
|
def __init__(
|
||||||
|
self, theme=0, themeFocus=3, themeUrgent=1, themeMode=2, customNames=dict()
|
||||||
|
):
|
||||||
I3Updater.__init__(self)
|
I3Updater.__init__(self)
|
||||||
Section.__init__(self)
|
Section.__init__(self)
|
||||||
self.themeNormal = theme
|
self.themeNormal = theme
|
||||||
|
@ -746,13 +763,14 @@ class I3WorkspacesProvider(Section, I3Updater):
|
||||||
parent.addSection(self.modeSection)
|
parent.addSection(self.modeSection)
|
||||||
self.initialPopulation(parent)
|
self.initialPopulation(parent)
|
||||||
|
|
||||||
|
|
||||||
class MpdProvider(Section, ThreadedUpdater):
|
class MpdProvider(Section, ThreadedUpdater):
|
||||||
# TODO FEAT More informations and controls
|
# TODO FEAT More informations and controls
|
||||||
|
|
||||||
MAX_LENGTH = 50
|
MAX_LENGTH = 50
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
self.mpd.connect('localhost', 6600)
|
self.mpd.connect("localhost", 6600)
|
||||||
|
|
||||||
def __init__(self, theme=None):
|
def __init__(self, theme=None):
|
||||||
ThreadedUpdater.__init__(self)
|
ThreadedUpdater.__init__(self)
|
||||||
|
@ -784,18 +802,16 @@ class MpdProvider(Section, ThreadedUpdater):
|
||||||
|
|
||||||
infosStr = " - ".join(infos)
|
infosStr = " - ".join(infos)
|
||||||
if len(infosStr) > MpdProvider.MAX_LENGTH:
|
if len(infosStr) > MpdProvider.MAX_LENGTH:
|
||||||
infosStr = infosStr[:MpdProvider.MAX_LENGTH-1] + '…'
|
infosStr = infosStr[: MpdProvider.MAX_LENGTH - 1] + "…"
|
||||||
|
|
||||||
return " {}".format(infosStr)
|
return " {}".format(infosStr)
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
try:
|
try:
|
||||||
self.mpd.idle('player')
|
self.mpd.idle("player")
|
||||||
self.refreshData()
|
self.refreshData()
|
||||||
except mpd.base.ConnectionError as e:
|
except mpd.base.ConnectionError as e:
|
||||||
log.warn(e, exc_info=True)
|
log.warn(e, exc_info=True)
|
||||||
self.connect()
|
self.connect()
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
log.error(e, exc_info=True)
|
log.error(e, exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,13 +11,14 @@ import coloredlogs
|
||||||
import i3ipc
|
import i3ipc
|
||||||
from display import Text
|
from display import Text
|
||||||
|
|
||||||
coloredlogs.install(level='DEBUG', fmt='%(levelname)s %(message)s')
|
coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s")
|
||||||
log = logging.getLogger()
|
log = logging.getLogger()
|
||||||
|
|
||||||
# TODO Sync bar update with PeriodicUpdater updates
|
# TODO Sync bar update with PeriodicUpdater updates
|
||||||
|
|
||||||
notBusy = threading.Event()
|
notBusy = threading.Event()
|
||||||
|
|
||||||
|
|
||||||
class Updater:
|
class Updater:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def init():
|
def init():
|
||||||
|
@ -52,8 +53,9 @@ class PeriodicUpdaterThread(threading.Thread):
|
||||||
counter = 0
|
counter = 0
|
||||||
while True:
|
while True:
|
||||||
notBusy.set()
|
notBusy.set()
|
||||||
if PeriodicUpdater.intervalsChanged \
|
if PeriodicUpdater.intervalsChanged.wait(
|
||||||
.wait(timeout=PeriodicUpdater.intervalStep):
|
timeout=PeriodicUpdater.intervalStep
|
||||||
|
):
|
||||||
# ↑ sleeps here
|
# ↑ sleeps here
|
||||||
notBusy.clear()
|
notBusy.clear()
|
||||||
PeriodicUpdater.intervalsChanged.clear()
|
PeriodicUpdater.intervalsChanged.clear()
|
||||||
|
@ -127,7 +129,6 @@ class PeriodicUpdater(Updater):
|
||||||
|
|
||||||
|
|
||||||
class InotifyUpdaterEventHandler(pyinotify.ProcessEvent):
|
class InotifyUpdaterEventHandler(pyinotify.ProcessEvent):
|
||||||
|
|
||||||
def process_default(self, event):
|
def process_default(self, event):
|
||||||
# DEBUG
|
# DEBUG
|
||||||
# from pprint import pprint
|
# from pprint import pprint
|
||||||
|
@ -155,8 +156,9 @@ class InotifyUpdater(Updater):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def init():
|
def init():
|
||||||
notifier = pyinotify.ThreadedNotifier(InotifyUpdater.wm,
|
notifier = pyinotify.ThreadedNotifier(
|
||||||
InotifyUpdaterEventHandler())
|
InotifyUpdater.wm, InotifyUpdaterEventHandler()
|
||||||
|
)
|
||||||
notifier.start()
|
notifier.start()
|
||||||
|
|
||||||
# TODO Mask for folders
|
# TODO Mask for folders
|
||||||
|
@ -174,8 +176,7 @@ class InotifyUpdater(Updater):
|
||||||
self.dirpath = os.path.dirname(path)
|
self.dirpath = os.path.dirname(path)
|
||||||
self.filename = os.path.basename(path)
|
self.filename = os.path.basename(path)
|
||||||
else:
|
else:
|
||||||
raise FileNotFoundError("No such file or directory: '{}'"
|
raise FileNotFoundError("No such file or directory: '{}'".format(path))
|
||||||
.format(path))
|
|
||||||
|
|
||||||
# Register watch action
|
# Register watch action
|
||||||
if self.dirpath not in InotifyUpdater.paths:
|
if self.dirpath not in InotifyUpdater.paths:
|
||||||
|
@ -266,4 +267,4 @@ class MergedUpdater(Updater):
|
||||||
updater.updateText = newUpdateText.__get__(updater, Updater)
|
updater.updateText = newUpdateText.__get__(updater, Updater)
|
||||||
|
|
||||||
self.updaters.append(updater)
|
self.updaters.append(updater)
|
||||||
self.texts[updater] = ''
|
self.texts[updater] = ""
|
||||||
|
|
|
@ -6,7 +6,7 @@ import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
coloredlogs.install(level='DEBUG', fmt='%(levelname)s %(message)s')
|
coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s")
|
||||||
log = logging.getLogger()
|
log = logging.getLogger()
|
||||||
|
|
||||||
# Coding conventions:
|
# Coding conventions:
|
||||||
|
@ -15,10 +15,10 @@ log = logging.getLogger()
|
||||||
# TODO Config arparse and pass args to the functions. No globals
|
# TODO Config arparse and pass args to the functions. No globals
|
||||||
|
|
||||||
# Finding directories
|
# Finding directories
|
||||||
assert 'HOME' in os.environ, "Home directory unknown"
|
assert "HOME" in os.environ, "Home directory unknown"
|
||||||
DOCS = os.path.realpath(os.path.join(os.environ['HOME'], 'Documents'))
|
DOCS = os.path.realpath(os.path.join(os.environ["HOME"], "Documents"))
|
||||||
assert os.path.isdir(DOCS), "Documents folder not found"
|
assert os.path.isdir(DOCS), "Documents folder not found"
|
||||||
ARCS = os.path.realpath(os.path.join(os.environ['HOME'], 'Archives'))
|
ARCS = os.path.realpath(os.path.join(os.environ["HOME"], "Archives"))
|
||||||
assert os.path.isdir(ARCS), "Archives folder not found"
|
assert os.path.isdir(ARCS), "Archives folder not found"
|
||||||
|
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ def dirRange(relpath):
|
||||||
res = list()
|
res = list()
|
||||||
|
|
||||||
for p in range(len(splits)):
|
for p in range(len(splits)):
|
||||||
partPath = os.path.join(*splits[:p+1])
|
partPath = os.path.join(*splits[: p + 1])
|
||||||
|
|
||||||
arcPath = os.path.join(os.path.join(ARCS, partPath))
|
arcPath = os.path.join(os.path.join(ARCS, partPath))
|
||||||
docPath = os.path.join(os.path.join(DOCS, partPath))
|
docPath = os.path.join(os.path.join(DOCS, partPath))
|
||||||
|
@ -36,6 +36,7 @@ def dirRange(relpath):
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
def travel(relpath):
|
def travel(relpath):
|
||||||
"""
|
"""
|
||||||
Dunno what this will do, let's write code and see.
|
Dunno what this will do, let's write code and see.
|
||||||
|
@ -60,8 +61,10 @@ def travel(relpath):
|
||||||
elif os.path.islink(docPath) and os.path.exists(arcPath):
|
elif os.path.islink(docPath) and os.path.exists(arcPath):
|
||||||
currentLink = os.readlink(docPath)
|
currentLink = os.readlink(docPath)
|
||||||
if currentLink != linkPath:
|
if currentLink != linkPath:
|
||||||
log.warning(f"'{docPath}' is pointing to '{currentLink}' " +
|
log.warning(
|
||||||
f"but should point to '{linkPath}'.")
|
f"'{docPath}' is pointing to '{currentLink}' "
|
||||||
|
+ f"but should point to '{linkPath}'."
|
||||||
|
)
|
||||||
# TODO Fixing if asked for
|
# TODO Fixing if asked for
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
log.debug("Early link already exists {docPath} → {arcPath}")
|
log.debug("Early link already exists {docPath} → {arcPath}")
|
||||||
|
@ -69,13 +72,11 @@ def travel(relpath):
|
||||||
elif not os.path.exists(docPath) and os.path.exists(arcPath):
|
elif not os.path.exists(docPath) and os.path.exists(arcPath):
|
||||||
log.debug("Only existing on archive side, linking")
|
log.debug("Only existing on archive side, linking")
|
||||||
print(f"ln -s {linkPath} {docPath}")
|
print(f"ln -s {linkPath} {docPath}")
|
||||||
elif os.path.exists(docPath) and not os.path.exists(arcPath) \
|
elif os.path.exists(docPath) and not os.path.exists(arcPath) and isLast:
|
||||||
and isLast:
|
|
||||||
log.debug("Only existing on doc side, moving and linking")
|
log.debug("Only existing on doc side, moving and linking")
|
||||||
print(f"mv {docPath} {arcPath}")
|
print(f"mv {docPath} {arcPath}")
|
||||||
print(f"ln -s {linkPath} {docPath}")
|
print(f"ln -s {linkPath} {docPath}")
|
||||||
elif os.path.exists(docPath) and not os.path.exists(arcPath) \
|
elif os.path.exists(docPath) and not os.path.exists(arcPath) and not isLast:
|
||||||
and not isLast:
|
|
||||||
raise NotImplementedError("Here comes the trouble")
|
raise NotImplementedError("Here comes the trouble")
|
||||||
else:
|
else:
|
||||||
log.error("Unhandled case")
|
log.error("Unhandled case")
|
||||||
|
@ -103,8 +104,10 @@ def ensureLink(relpath):
|
||||||
if os.path.islink(docPath):
|
if os.path.islink(docPath):
|
||||||
currentLink = os.readlink(docPath)
|
currentLink = os.readlink(docPath)
|
||||||
if currentLink != linkPath:
|
if currentLink != linkPath:
|
||||||
log.warning(f"'{docPath}' is pointing to '{currentLink}' " +
|
log.warning(
|
||||||
f"but should point to '{linkPath}'. Fixing")
|
f"'{docPath}' is pointing to '{currentLink}' "
|
||||||
|
+ f"but should point to '{linkPath}'. Fixing"
|
||||||
|
)
|
||||||
if args.dry:
|
if args.dry:
|
||||||
print(f"rm {docPath}")
|
print(f"rm {docPath}")
|
||||||
else:
|
else:
|
||||||
|
@ -117,10 +120,13 @@ def ensureLink(relpath):
|
||||||
elif os.path.isdir(docPath):
|
elif os.path.isdir(docPath):
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(f"'{docPath}' exists and is not a directory " +
|
raise RuntimeError(
|
||||||
f"or a link. Unable to link it to '{linkPath}'")
|
f"'{docPath}' exists and is not a directory "
|
||||||
raise RuntimeError(f"'{docPath}' is a directory. Unable to link it to " +
|
+ f"or a link. Unable to link it to '{linkPath}'"
|
||||||
f"'{linkPath}'")
|
)
|
||||||
|
raise RuntimeError(
|
||||||
|
f"'{docPath}' is a directory. Unable to link it to " + f"'{linkPath}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def archive(docdir):
|
def archive(docdir):
|
||||||
|
@ -134,8 +140,8 @@ def archive(docdir):
|
||||||
print("ARC", reldir)
|
print("ARC", reldir)
|
||||||
|
|
||||||
arcdir = os.path.join(ARCS, reldir)
|
arcdir = os.path.join(ARCS, reldir)
|
||||||
parentArcdir = os.path.realpath(os.path.join(arcdir, '..'))
|
parentArcdir = os.path.realpath(os.path.join(arcdir, ".."))
|
||||||
parentDocdir = os.path.realpath(os.path.join(docdir, '..'))
|
parentDocdir = os.path.realpath(os.path.join(docdir, ".."))
|
||||||
linkDest = os.path.relpath(arcdir, parentDocdir)
|
linkDest = os.path.relpath(arcdir, parentDocdir)
|
||||||
|
|
||||||
# BULLSHIT
|
# BULLSHIT
|
||||||
|
@ -172,11 +178,15 @@ def unarchive(arcdir):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="Place a folder in ~/Documents in ~/Documents/Archives and symlink it")
|
parser = argparse.ArgumentParser(
|
||||||
parser.add_argument('dir', metavar='DIRECTORY', type=str, help="The directory to archive")
|
description="Place a folder in ~/Documents in ~/Documents/Archives and symlink it"
|
||||||
parser.add_argument('-d', '--dry', action='store_true')
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"dir", metavar="DIRECTORY", type=str, help="The directory to archive"
|
||||||
|
)
|
||||||
|
parser.add_argument("-d", "--dry", action="store_true")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
args.dry = True # DEBUG
|
args.dry = True # DEBUG
|
||||||
|
|
||||||
# archive(args.dir)
|
# archive(args.dir)
|
||||||
ensureLink(args.dir)
|
ensureLink(args.dir)
|
||||||
|
|
|
@ -14,7 +14,7 @@ import json
|
||||||
import statistics
|
import statistics
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
coloredlogs.install(level='DEBUG', fmt='%(levelname)s %(message)s')
|
coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s")
|
||||||
log = logging.getLogger()
|
log = logging.getLogger()
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
|
@ -34,15 +34,15 @@ def videoMetadata(filename):
|
||||||
p.check_returncode()
|
p.check_returncode()
|
||||||
metadataRaw = p.stdout
|
metadataRaw = p.stdout
|
||||||
data = dict()
|
data = dict()
|
||||||
for metadataLine in metadataRaw.split(b'\n'):
|
for metadataLine in metadataRaw.split(b"\n"):
|
||||||
# Skip empty lines
|
# Skip empty lines
|
||||||
if not len(metadataLine):
|
if not len(metadataLine):
|
||||||
continue
|
continue
|
||||||
# Skip comments
|
# Skip comments
|
||||||
if metadataLine.startswith(b';'):
|
if metadataLine.startswith(b";"):
|
||||||
continue
|
continue
|
||||||
# Parse key-value
|
# Parse key-value
|
||||||
metadataLineSplit = metadataLine.split(b'=')
|
metadataLineSplit = metadataLine.split(b"=")
|
||||||
if len(metadataLineSplit) != 2:
|
if len(metadataLineSplit) != 2:
|
||||||
log.warning("Unparsed metadata line: `{}`".format(metadataLine))
|
log.warning("Unparsed metadata line: `{}`".format(metadataLine))
|
||||||
continue
|
continue
|
||||||
|
@ -52,6 +52,7 @@ def videoMetadata(filename):
|
||||||
data[key] = val
|
data[key] = val
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def videoInfos(filename):
|
def videoInfos(filename):
|
||||||
assert os.path.isfile(filename)
|
assert os.path.isfile(filename)
|
||||||
cmd = ["ffprobe", filename, "-print_format", "json", "-show_streams"]
|
cmd = ["ffprobe", filename, "-print_format", "json", "-show_streams"]
|
||||||
|
@ -61,7 +62,10 @@ def videoInfos(filename):
|
||||||
infos = json.loads(infosRaw)
|
infos = json.loads(infosRaw)
|
||||||
return infos
|
return infos
|
||||||
|
|
||||||
|
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
|
||||||
|
|
||||||
def streamDuration(stream):
|
def streamDuration(stream):
|
||||||
if "duration" in stream:
|
if "duration" in stream:
|
||||||
return float(stream["duration"])
|
return float(stream["duration"])
|
||||||
|
@ -77,13 +81,14 @@ def streamDuration(stream):
|
||||||
else:
|
else:
|
||||||
raise KeyError("Can't find duration information in stream")
|
raise KeyError("Can't find duration information in stream")
|
||||||
|
|
||||||
|
|
||||||
def videoDuration(filename):
|
def videoDuration(filename):
|
||||||
# TODO Doesn't work with VP8 / webm
|
# TODO Doesn't work with VP8 / webm
|
||||||
infos = videoInfos(filename)
|
infos = videoInfos(filename)
|
||||||
durations = [streamDuration(stream) for stream in infos["streams"]]
|
durations = [streamDuration(stream) for stream in infos["streams"]]
|
||||||
dev = statistics.stdev(durations)
|
dev = statistics.stdev(durations)
|
||||||
assert dev <= DURATION_MAX_DEV, "Too much deviation ({} s)".format(dev)
|
assert dev <= DURATION_MAX_DEV, "Too much deviation ({} s)".format(dev)
|
||||||
return sum(durations)/len(durations)
|
return sum(durations) / len(durations)
|
||||||
|
|
||||||
|
|
||||||
todos = set()
|
todos = set()
|
||||||
|
@ -130,13 +135,12 @@ for root, inputName in progressbar.progressbar(allVideos):
|
||||||
meta = videoMetadata(inputFull)
|
meta = videoMetadata(inputFull)
|
||||||
|
|
||||||
# If it has the field with the original file
|
# If it has the field with the original file
|
||||||
if 'original' in meta:
|
if "original" in meta:
|
||||||
# Skip file
|
# Skip file
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
assert not os.path.isfile(outputFull), outputFull + " exists"
|
assert not os.path.isfile(outputFull), outputFull + " exists"
|
||||||
|
|
||||||
|
|
||||||
size = os.stat(inputFull).st_size
|
size = os.stat(inputFull).st_size
|
||||||
try:
|
try:
|
||||||
duration = videoDuration(inputFull)
|
duration = videoDuration(inputFull)
|
||||||
|
@ -151,7 +155,11 @@ for root, inputName in progressbar.progressbar(allVideos):
|
||||||
totalSize += size
|
totalSize += size
|
||||||
todos.add(todo)
|
todos.add(todo)
|
||||||
|
|
||||||
log.info("Converting {} videos ({})".format(len(todos), datetime.timedelta(seconds=totalDuration)))
|
log.info(
|
||||||
|
"Converting {} videos ({})".format(
|
||||||
|
len(todos), datetime.timedelta(seconds=totalDuration)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# From https://stackoverflow.com/a/3431838
|
# From https://stackoverflow.com/a/3431838
|
||||||
def sha256(fname):
|
def sha256(fname):
|
||||||
|
@ -161,17 +169,30 @@ def sha256(fname):
|
||||||
hash_sha256.update(chunk)
|
hash_sha256.update(chunk)
|
||||||
return hash_sha256.hexdigest()
|
return hash_sha256.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
# Progress bar things
|
# Progress bar things
|
||||||
totalDataSize = progressbar.widgets.DataSize()
|
totalDataSize = progressbar.widgets.DataSize()
|
||||||
totalDataSize.variable = 'max_value'
|
totalDataSize.variable = "max_value"
|
||||||
barWidgets = [progressbar.widgets.DataSize(), ' of ', totalDataSize, ' ', progressbar.widgets.Bar(), ' ', progressbar.widgets.FileTransferSpeed(), ' ', progressbar.widgets.AdaptiveETA()]
|
barWidgets = [
|
||||||
|
progressbar.widgets.DataSize(),
|
||||||
|
" of ",
|
||||||
|
totalDataSize,
|
||||||
|
" ",
|
||||||
|
progressbar.widgets.Bar(),
|
||||||
|
" ",
|
||||||
|
progressbar.widgets.FileTransferSpeed(),
|
||||||
|
" ",
|
||||||
|
progressbar.widgets.AdaptiveETA(),
|
||||||
|
]
|
||||||
bar = progressbar.DataTransferBar(max_value=totalSize, widgets=barWidgets)
|
bar = progressbar.DataTransferBar(max_value=totalSize, widgets=barWidgets)
|
||||||
bar.start()
|
bar.start()
|
||||||
processedSize = 0
|
processedSize = 0
|
||||||
|
|
||||||
|
|
||||||
for inputFull, originalFull, outputFull, size, duration in todos:
|
for inputFull, originalFull, outputFull, size, duration in todos:
|
||||||
tmpfile = tempfile.mkstemp(prefix="compressPictureMovies", suffix="."+OUTPUT_EXTENSION)[1]
|
tmpfile = tempfile.mkstemp(
|
||||||
|
prefix="compressPictureMovies", suffix="." + OUTPUT_EXTENSION
|
||||||
|
)[1]
|
||||||
try:
|
try:
|
||||||
# Calculate the sum of the original file
|
# Calculate the sum of the original file
|
||||||
checksum = sha256(inputFull)
|
checksum = sha256(inputFull)
|
||||||
|
@ -180,7 +201,12 @@ for inputFull, originalFull, outputFull, size, duration in todos:
|
||||||
originalRel = os.path.relpath(originalFull, ORIGINAL_FOLDER)
|
originalRel = os.path.relpath(originalFull, ORIGINAL_FOLDER)
|
||||||
originalContent = "{} {}".format(originalRel, checksum)
|
originalContent = "{} {}".format(originalRel, checksum)
|
||||||
metadataCmd = ["-metadata", 'original="{}"'.format(originalContent)]
|
metadataCmd = ["-metadata", 'original="{}"'.format(originalContent)]
|
||||||
cmd = ["ffmpeg", "-hide_banner", "-y", "-i", inputFull] + OUTPUT_FFMPEG_PARAMETERS + metadataCmd + [tmpfile]
|
cmd = (
|
||||||
|
["ffmpeg", "-hide_banner", "-y", "-i", inputFull]
|
||||||
|
+ OUTPUT_FFMPEG_PARAMETERS
|
||||||
|
+ metadataCmd
|
||||||
|
+ [tmpfile]
|
||||||
|
)
|
||||||
p = subprocess.run(cmd)
|
p = subprocess.run(cmd)
|
||||||
p.check_returncode()
|
p.check_returncode()
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import random
|
import random
|
||||||
|
@ -21,7 +21,7 @@ for line in sys.stdin:
|
||||||
else:
|
else:
|
||||||
wrd = list(word)
|
wrd = list(word)
|
||||||
random.shuffle(wrd)
|
random.shuffle(wrd)
|
||||||
nl += ''.join(wrd)
|
nl += "".join(wrd)
|
||||||
nl += c
|
nl += c
|
||||||
word = ""
|
word = ""
|
||||||
grace = True
|
grace = True
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
@ -7,7 +7,7 @@ import logging
|
||||||
|
|
||||||
import coloredlogs
|
import coloredlogs
|
||||||
|
|
||||||
coloredlogs.install(level='DEBUG', fmt='%(levelname)s %(message)s')
|
coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s")
|
||||||
log = logging.getLogger()
|
log = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ def duration_file(path: str) -> float:
|
||||||
ret = run.stdout.decode().strip()
|
ret = run.stdout.decode().strip()
|
||||||
if run.returncode != 0:
|
if run.returncode != 0:
|
||||||
log.warning(f"{path}: unable to get duration")
|
log.warning(f"{path}: unable to get duration")
|
||||||
elif ret == 'N/A':
|
elif ret == "N/A":
|
||||||
log.warning(f"{path}: has no duration")
|
log.warning(f"{path}: has no duration")
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -45,7 +45,7 @@ import notmuch
|
||||||
import progressbar
|
import progressbar
|
||||||
import xdg.BaseDirectory
|
import xdg.BaseDirectory
|
||||||
|
|
||||||
MailLocation = typing.NewType('MailLocation', typing.Tuple[str, str, str])
|
MailLocation = typing.NewType("MailLocation", typing.Tuple[str, str, str])
|
||||||
# MessageAction = typing.Callable[[notmuch.Message], None]
|
# MessageAction = typing.Callable[[notmuch.Message], None]
|
||||||
|
|
||||||
|
|
||||||
|
@ -106,7 +106,8 @@ class MelEngine:
|
||||||
assert not self.database
|
assert not self.database
|
||||||
self.log.info("Indexing mails")
|
self.log.info("Indexing mails")
|
||||||
notmuch_config_file = os.path.expanduser(
|
notmuch_config_file = os.path.expanduser(
|
||||||
"~/.config/notmuch-config") # TODO Better
|
"~/.config/notmuch-config"
|
||||||
|
) # TODO Better
|
||||||
cmd = ["notmuch", "--config", notmuch_config_file, "new"]
|
cmd = ["notmuch", "--config", notmuch_config_file, "new"]
|
||||||
self.log.debug(" ".join(cmd))
|
self.log.debug(" ".join(cmd))
|
||||||
subprocess.run(cmd, check=True)
|
subprocess.run(cmd, check=True)
|
||||||
|
@ -117,7 +118,8 @@ class MelEngine:
|
||||||
"""
|
"""
|
||||||
assert self.config
|
assert self.config
|
||||||
storage_path = os.path.realpath(
|
storage_path = os.path.realpath(
|
||||||
os.path.expanduser(self.config["GENERAL"]["storage"]))
|
os.path.expanduser(self.config["GENERAL"]["storage"])
|
||||||
|
)
|
||||||
folders = list()
|
folders = list()
|
||||||
for account in self.accounts:
|
for account in self.accounts:
|
||||||
storage_path_account = os.path.join(storage_path, account)
|
storage_path_account = os.path.join(storage_path, account)
|
||||||
|
@ -125,9 +127,9 @@ class MelEngine:
|
||||||
if "cur" not in dirs or "new" not in dirs or "tmp" not in dirs:
|
if "cur" not in dirs or "new" not in dirs or "tmp" not in dirs:
|
||||||
continue
|
continue
|
||||||
assert root.startswith(storage_path)
|
assert root.startswith(storage_path)
|
||||||
path = root[len(storage_path):]
|
path = root[len(storage_path) :]
|
||||||
path_split = path.split('/')
|
path_split = path.split("/")
|
||||||
if path_split[0] == '':
|
if path_split[0] == "":
|
||||||
path_split = path_split[1:]
|
path_split = path_split[1:]
|
||||||
folders.append(tuple(path_split))
|
folders.append(tuple(path_split))
|
||||||
return folders
|
return folders
|
||||||
|
@ -138,8 +140,11 @@ class MelEngine:
|
||||||
Be sure to require only in the mode you want to avoid deadlocks.
|
Be sure to require only in the mode you want to avoid deadlocks.
|
||||||
"""
|
"""
|
||||||
assert self.config
|
assert self.config
|
||||||
mode = notmuch.Database.MODE.READ_WRITE if write \
|
mode = (
|
||||||
|
notmuch.Database.MODE.READ_WRITE
|
||||||
|
if write
|
||||||
else notmuch.Database.MODE.READ_ONLY
|
else notmuch.Database.MODE.READ_ONLY
|
||||||
|
)
|
||||||
if self.database:
|
if self.database:
|
||||||
# If the requested mode is the one already present,
|
# If the requested mode is the one already present,
|
||||||
# or we request read when it's already write, do nothing
|
# or we request read when it's already write, do nothing
|
||||||
|
@ -149,7 +154,8 @@ class MelEngine:
|
||||||
self.close_database()
|
self.close_database()
|
||||||
self.log.info("Opening database in mode %s", mode)
|
self.log.info("Opening database in mode %s", mode)
|
||||||
db_path = os.path.realpath(
|
db_path = os.path.realpath(
|
||||||
os.path.expanduser(self.config["GENERAL"]["storage"]))
|
os.path.expanduser(self.config["GENERAL"]["storage"])
|
||||||
|
)
|
||||||
self.database = notmuch.Database(mode=mode, path=db_path)
|
self.database = notmuch.Database(mode=mode, path=db_path)
|
||||||
|
|
||||||
def close_database(self) -> None:
|
def close_database(self) -> None:
|
||||||
|
@ -171,13 +177,13 @@ class MelEngine:
|
||||||
assert self.database
|
assert self.database
|
||||||
base = self.database.get_path()
|
base = self.database.get_path()
|
||||||
assert path.startswith(base)
|
assert path.startswith(base)
|
||||||
path = path[len(base):]
|
path = path[len(base) :]
|
||||||
path_split = path.split('/')
|
path_split = path.split("/")
|
||||||
mailbox = path_split[1]
|
mailbox = path_split[1]
|
||||||
assert mailbox in self.accounts
|
assert mailbox in self.accounts
|
||||||
state = path_split[-1]
|
state = path_split[-1]
|
||||||
folder = tuple(path_split[2:-1])
|
folder = tuple(path_split[2:-1])
|
||||||
assert state in {'cur', 'tmp', 'new'}
|
assert state in {"cur", "tmp", "new"}
|
||||||
return (mailbox, folder, state)
|
return (mailbox, folder, state)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -185,8 +191,11 @@ class MelEngine:
|
||||||
"""
|
"""
|
||||||
Tells if the provided string is a valid UID.
|
Tells if the provided string is a valid UID.
|
||||||
"""
|
"""
|
||||||
return isinstance(uid, str) and len(uid) == 12 \
|
return (
|
||||||
and bool(re.match('^[a-zA-Z0-9+/]{12}$', uid))
|
isinstance(uid, str)
|
||||||
|
and len(uid) == 12
|
||||||
|
and bool(re.match("^[a-zA-Z0-9+/]{12}$", uid))
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def extract_email(field: str) -> str:
|
def extract_email(field: str) -> str:
|
||||||
|
@ -197,9 +206,9 @@ class MelEngine:
|
||||||
# TODO Can be made better (extract name and email)
|
# TODO Can be made better (extract name and email)
|
||||||
# Also what happens with multiple dests?
|
# Also what happens with multiple dests?
|
||||||
try:
|
try:
|
||||||
sta = field.index('<')
|
sta = field.index("<")
|
||||||
sto = field.index('>')
|
sto = field.index(">")
|
||||||
return field[sta+1:sto]
|
return field[sta + 1 : sto]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return field
|
return field
|
||||||
|
|
||||||
|
@ -211,8 +220,9 @@ class MelEngine:
|
||||||
|
|
||||||
# Search-friendly folder name
|
# Search-friendly folder name
|
||||||
slug_folder_list = list()
|
slug_folder_list = list()
|
||||||
for fold_index, fold in [(fold_index, folder[fold_index])
|
for fold_index, fold in [
|
||||||
for fold_index in range(len(folder))]:
|
(fold_index, folder[fold_index]) for fold_index in range(len(folder))
|
||||||
|
]:
|
||||||
if fold_index == 0 and len(folder) > 1 and fold == "INBOX":
|
if fold_index == 0 and len(folder) > 1 and fold == "INBOX":
|
||||||
continue
|
continue
|
||||||
slug_folder_list.append(fold.upper())
|
slug_folder_list.append(fold.upper())
|
||||||
|
@ -229,14 +239,15 @@ class MelEngine:
|
||||||
msg.add_tag(tag)
|
msg.add_tag(tag)
|
||||||
elif not condition and tag in tags:
|
elif not condition and tag in tags:
|
||||||
msg.remove_tag(tag)
|
msg.remove_tag(tag)
|
||||||
expeditor = MelEngine.extract_email(msg.get_header('from'))
|
|
||||||
|
|
||||||
tag_if('inbox', slug_folder[0] == 'INBOX')
|
expeditor = MelEngine.extract_email(msg.get_header("from"))
|
||||||
tag_if('spam', slug_folder[0] in ('JUNK', 'SPAM'))
|
|
||||||
tag_if('deleted', slug_folder[0] == 'TRASH')
|
tag_if("inbox", slug_folder[0] == "INBOX")
|
||||||
tag_if('draft', slug_folder[0] == 'DRAFTS')
|
tag_if("spam", slug_folder[0] in ("JUNK", "SPAM"))
|
||||||
tag_if('sent', expeditor in self.aliases)
|
tag_if("deleted", slug_folder[0] == "TRASH")
|
||||||
tag_if('unprocessed', False)
|
tag_if("draft", slug_folder[0] == "DRAFTS")
|
||||||
|
tag_if("sent", expeditor in self.aliases)
|
||||||
|
tag_if("unprocessed", False)
|
||||||
|
|
||||||
# UID
|
# UID
|
||||||
uid = msg.get_header("X-TUID")
|
uid = msg.get_header("X-TUID")
|
||||||
|
@ -244,17 +255,23 @@ class MelEngine:
|
||||||
# TODO Happens to sent mails but should it?
|
# TODO Happens to sent mails but should it?
|
||||||
print(f"{msg.get_filename()} has no UID!")
|
print(f"{msg.get_filename()} has no UID!")
|
||||||
return
|
return
|
||||||
uidtag = 'tuid{}'.format(uid)
|
uidtag = "tuid{}".format(uid)
|
||||||
# Remove eventual others UID
|
# Remove eventual others UID
|
||||||
for tag in tags:
|
for tag in tags:
|
||||||
if tag.startswith('tuid') and tag != uidtag:
|
if tag.startswith("tuid") and tag != uidtag:
|
||||||
msg.remove_tag(tag)
|
msg.remove_tag(tag)
|
||||||
msg.add_tag(uidtag)
|
msg.add_tag(uidtag)
|
||||||
|
|
||||||
def apply_msgs(self, query_str: str, action: typing.Callable,
|
def apply_msgs(
|
||||||
*args: typing.Any, show_progress: bool = False,
|
self,
|
||||||
write: bool = False, close_db: bool = True,
|
query_str: str,
|
||||||
**kwargs: typing.Any) -> int:
|
action: typing.Callable,
|
||||||
|
*args: typing.Any,
|
||||||
|
show_progress: bool = False,
|
||||||
|
write: bool = False,
|
||||||
|
close_db: bool = True,
|
||||||
|
**kwargs: typing.Any,
|
||||||
|
) -> int:
|
||||||
"""
|
"""
|
||||||
Run a function on the messages selected by the given query.
|
Run a function on the messages selected by the given query.
|
||||||
"""
|
"""
|
||||||
|
@ -267,8 +284,11 @@ class MelEngine:
|
||||||
elements = query.search_messages()
|
elements = query.search_messages()
|
||||||
nb_msgs = query.count_messages()
|
nb_msgs = query.count_messages()
|
||||||
|
|
||||||
iterator = progressbar.progressbar(elements, max_value=nb_msgs) \
|
iterator = (
|
||||||
if show_progress and nb_msgs else elements
|
progressbar.progressbar(elements, max_value=nb_msgs)
|
||||||
|
if show_progress and nb_msgs
|
||||||
|
else elements
|
||||||
|
)
|
||||||
|
|
||||||
self.log.info("Executing %s", action)
|
self.log.info("Executing %s", action)
|
||||||
for msg in iterator:
|
for msg in iterator:
|
||||||
|
@ -296,8 +316,9 @@ class MelOutput:
|
||||||
WIDTH_FIXED = 31
|
WIDTH_FIXED = 31
|
||||||
WIDTH_RATIO_DEST_SUBJECT = 0.3
|
WIDTH_RATIO_DEST_SUBJECT = 0.3
|
||||||
|
|
||||||
def compute_line_format(self) -> typing.Tuple[typing.Optional[int],
|
def compute_line_format(
|
||||||
typing.Optional[int]]:
|
self,
|
||||||
|
) -> typing.Tuple[typing.Optional[int], typing.Optional[int]]:
|
||||||
"""
|
"""
|
||||||
Based on the terminal width, assign the width of flexible columns.
|
Based on the terminal width, assign the width of flexible columns.
|
||||||
"""
|
"""
|
||||||
|
@ -332,12 +353,12 @@ class MelOutput:
|
||||||
"""
|
"""
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
if now - date < datetime.timedelta(days=1):
|
if now - date < datetime.timedelta(days=1):
|
||||||
return date.strftime('%H:%M:%S')
|
return date.strftime("%H:%M:%S")
|
||||||
if now - date < datetime.timedelta(days=28):
|
if now - date < datetime.timedelta(days=28):
|
||||||
return date.strftime('%d %H:%M')
|
return date.strftime("%d %H:%M")
|
||||||
if now - date < datetime.timedelta(days=365):
|
if now - date < datetime.timedelta(days=365):
|
||||||
return date.strftime('%m-%d %H')
|
return date.strftime("%m-%d %H")
|
||||||
return date.strftime('%y-%m-%d')
|
return date.strftime("%y-%m-%d")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def clip_text(size: typing.Optional[int], text: str) -> str:
|
def clip_text(size: typing.Optional[int], text: str) -> str:
|
||||||
|
@ -352,28 +373,28 @@ class MelOutput:
|
||||||
if length == size:
|
if length == size:
|
||||||
return text
|
return text
|
||||||
if length > size:
|
if length > size:
|
||||||
return text[:size-1] + '…'
|
return text[: size - 1] + "…"
|
||||||
return text + ' ' * (size - length)
|
return text + " " * (size - length)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def chunks(iterable: str, chunk_size: int) -> typing.Iterable[str]:
|
def chunks(iterable: str, chunk_size: int) -> typing.Iterable[str]:
|
||||||
"""Yield successive chunk_size-sized chunks from iterable."""
|
"""Yield successive chunk_size-sized chunks from iterable."""
|
||||||
# From https://stackoverflow.com/a/312464
|
# From https://stackoverflow.com/a/312464
|
||||||
for i in range(0, len(iterable), chunk_size):
|
for i in range(0, len(iterable), chunk_size):
|
||||||
yield iterable[i:i + chunk_size]
|
yield iterable[i : i + chunk_size]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def sizeof_fmt(num: int, suffix: str = 'B') -> str:
|
def sizeof_fmt(num: int, suffix: str = "B") -> str:
|
||||||
"""
|
"""
|
||||||
Print the given size in a human-readable format.
|
Print the given size in a human-readable format.
|
||||||
"""
|
"""
|
||||||
remainder = float(num)
|
remainder = float(num)
|
||||||
# From https://stackoverflow.com/a/1094933
|
# From https://stackoverflow.com/a/1094933
|
||||||
for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
|
for unit in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]:
|
||||||
if abs(remainder) < 1024.0:
|
if abs(remainder) < 1024.0:
|
||||||
return "%3.1f %s%s" % (remainder, unit, suffix)
|
return "%3.1f %s%s" % (remainder, unit, suffix)
|
||||||
remainder /= 1024.0
|
remainder /= 1024.0
|
||||||
return "%.1f %s%s" % (remainder, 'Yi', suffix)
|
return "%.1f %s%s" % (remainder, "Yi", suffix)
|
||||||
|
|
||||||
def get_mailbox_color(self, mailbox: str) -> str:
|
def get_mailbox_color(self, mailbox: str) -> str:
|
||||||
"""
|
"""
|
||||||
|
@ -381,7 +402,7 @@ class MelOutput:
|
||||||
string with ASCII escape codes.
|
string with ASCII escape codes.
|
||||||
"""
|
"""
|
||||||
if not self.is_tty:
|
if not self.is_tty:
|
||||||
return ''
|
return ""
|
||||||
if mailbox not in self.mailbox_colors:
|
if mailbox not in self.mailbox_colors:
|
||||||
# RGB colors (not supported everywhere)
|
# RGB colors (not supported everywhere)
|
||||||
# color_str = self.config[mailbox]["color"]
|
# color_str = self.config[mailbox]["color"]
|
||||||
|
@ -406,21 +427,24 @@ class MelOutput:
|
||||||
line = ""
|
line = ""
|
||||||
tags = set(msg.get_tags())
|
tags = set(msg.get_tags())
|
||||||
mailbox, _, _ = self.engine.get_location(msg)
|
mailbox, _, _ = self.engine.get_location(msg)
|
||||||
if 'unread' in tags or 'flagged' in tags:
|
if "unread" in tags or "flagged" in tags:
|
||||||
line += colorama.Style.BRIGHT
|
line += colorama.Style.BRIGHT
|
||||||
# if 'flagged' in tags:
|
# if 'flagged' in tags:
|
||||||
# line += colorama.Style.BRIGHT
|
# line += colorama.Style.BRIGHT
|
||||||
# if 'unread' not in tags:
|
# if 'unread' not in tags:
|
||||||
# line += colorama.Style.DIM
|
# line += colorama.Style.DIM
|
||||||
line += colorama.Back.LIGHTBLACK_EX if self.light_background \
|
line += (
|
||||||
|
colorama.Back.LIGHTBLACK_EX
|
||||||
|
if self.light_background
|
||||||
else colorama.Back.BLACK
|
else colorama.Back.BLACK
|
||||||
|
)
|
||||||
self.light_background = not self.light_background
|
self.light_background = not self.light_background
|
||||||
line += self.get_mailbox_color(mailbox)
|
line += self.get_mailbox_color(mailbox)
|
||||||
|
|
||||||
# UID
|
# UID
|
||||||
uid = None
|
uid = None
|
||||||
for tag in tags:
|
for tag in tags:
|
||||||
if tag.startswith('tuid'):
|
if tag.startswith("tuid"):
|
||||||
uid = tag[4:]
|
uid = tag[4:]
|
||||||
assert uid, f"No UID for message: {msg}."
|
assert uid, f"No UID for message: {msg}."
|
||||||
assert MelEngine.is_uid(uid), f"{uid} {type(uid)} is not a valid UID."
|
assert MelEngine.is_uid(uid), f"{uid} {type(uid)} is not a valid UID."
|
||||||
|
@ -434,8 +458,9 @@ class MelOutput:
|
||||||
# Icons
|
# Icons
|
||||||
line += sep + colorama.Fore.RED
|
line += sep + colorama.Fore.RED
|
||||||
|
|
||||||
def tags2col1(tag1: str, tag2: str,
|
def tags2col1(
|
||||||
characters: typing.Tuple[str, str, str, str]) -> None:
|
tag1: str, tag2: str, characters: typing.Tuple[str, str, str, str]
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Show the presence/absence of two tags with one character.
|
Show the presence/absence of two tags with one character.
|
||||||
"""
|
"""
|
||||||
|
@ -452,14 +477,14 @@ class MelOutput:
|
||||||
else:
|
else:
|
||||||
line += none
|
line += none
|
||||||
|
|
||||||
tags2col1('spam', 'draft', ('?', 'S', 'D', ' '))
|
tags2col1("spam", "draft", ("?", "S", "D", " "))
|
||||||
tags2col1('attachment', 'encrypted', ('E', 'A', 'E', ' '))
|
tags2col1("attachment", "encrypted", ("E", "A", "E", " "))
|
||||||
tags2col1('unread', 'flagged', ('!', 'U', 'F', ' '))
|
tags2col1("unread", "flagged", ("!", "U", "F", " "))
|
||||||
tags2col1('sent', 'replied', ('?', '↑', '↪', ' '))
|
tags2col1("sent", "replied", ("?", "↑", "↪", " "))
|
||||||
|
|
||||||
# Opposed
|
# Opposed
|
||||||
line += sep + colorama.Fore.BLUE
|
line += sep + colorama.Fore.BLUE
|
||||||
if 'sent' in tags:
|
if "sent" in tags:
|
||||||
dest = msg.get_header("to")
|
dest = msg.get_header("to")
|
||||||
else:
|
else:
|
||||||
dest = msg.get_header("from")
|
dest = msg.get_header("from")
|
||||||
|
@ -483,11 +508,10 @@ class MelOutput:
|
||||||
expd = msg.get_header("from")
|
expd = msg.get_header("from")
|
||||||
account, _, _ = self.engine.get_location(msg)
|
account, _, _ = self.engine.get_location(msg)
|
||||||
|
|
||||||
summary = '{} (<i>{}</i>)'.format(html.escape(expd), account)
|
summary = "{} (<i>{}</i>)".format(html.escape(expd), account)
|
||||||
body = html.escape(subject)
|
body = html.escape(subject)
|
||||||
cmd = ["notify-send", "-u", "low", "-i",
|
cmd = ["notify-send", "-u", "low", "-i", "mail-message-new", summary, body]
|
||||||
"mail-message-new", summary, body]
|
print(" ".join(cmd))
|
||||||
print(' '.join(cmd))
|
|
||||||
subprocess.run(cmd, check=False)
|
subprocess.run(cmd, check=False)
|
||||||
|
|
||||||
def notify_all(self) -> None:
|
def notify_all(self) -> None:
|
||||||
|
@ -497,12 +521,26 @@ class MelOutput:
|
||||||
since it should be marked as processed right after.
|
since it should be marked as processed right after.
|
||||||
"""
|
"""
|
||||||
nb_msgs = self.engine.apply_msgs(
|
nb_msgs = self.engine.apply_msgs(
|
||||||
'tag:unread and tag:unprocessed', self.notify_msg)
|
"tag:unread and tag:unprocessed", self.notify_msg
|
||||||
|
)
|
||||||
if nb_msgs > 0:
|
if nb_msgs > 0:
|
||||||
self.log.info(
|
self.log.info("Playing notification sound (%d new message(s))", nb_msgs)
|
||||||
"Playing notification sound (%d new message(s))", nb_msgs)
|
cmd = [
|
||||||
cmd = ["play", "-n", "synth", "sine", "E4", "sine", "A5",
|
"play",
|
||||||
"remix", "1-2", "fade", "0.5", "1.2", "0.5", "2"]
|
"-n",
|
||||||
|
"synth",
|
||||||
|
"sine",
|
||||||
|
"E4",
|
||||||
|
"sine",
|
||||||
|
"A5",
|
||||||
|
"remix",
|
||||||
|
"1-2",
|
||||||
|
"fade",
|
||||||
|
"0.5",
|
||||||
|
"1.2",
|
||||||
|
"0.5",
|
||||||
|
"2",
|
||||||
|
]
|
||||||
subprocess.run(cmd, check=False)
|
subprocess.run(cmd, check=False)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -510,46 +548,62 @@ class MelOutput:
|
||||||
"""
|
"""
|
||||||
Return split header values in a contiguous string.
|
Return split header values in a contiguous string.
|
||||||
"""
|
"""
|
||||||
return val.replace('\n', '').replace('\t', '').strip()
|
return val.replace("\n", "").replace("\t", "").strip()
|
||||||
|
|
||||||
PART_MULTI_FORMAT = colorama.Fore.BLUE + \
|
PART_MULTI_FORMAT = (
|
||||||
'{count} {indent}+ {typ}' + colorama.Style.RESET_ALL
|
colorama.Fore.BLUE + "{count} {indent}+ {typ}" + colorama.Style.RESET_ALL
|
||||||
PART_LEAF_FORMAT = colorama.Fore.BLUE + \
|
)
|
||||||
'{count} {indent}→ {desc} ({typ}; {size})' + \
|
PART_LEAF_FORMAT = (
|
||||||
colorama.Style.RESET_ALL
|
colorama.Fore.BLUE
|
||||||
|
+ "{count} {indent}→ {desc} ({typ}; {size})"
|
||||||
|
+ colorama.Style.RESET_ALL
|
||||||
|
)
|
||||||
|
|
||||||
def show_parts_tree(self, part: email.message.Message,
|
def show_parts_tree(
|
||||||
depth: int = 0, count: int = 1) -> int:
|
self, part: email.message.Message, depth: int = 0, count: int = 1
|
||||||
|
) -> int:
|
||||||
"""
|
"""
|
||||||
Show a tree of the parts contained in a message.
|
Show a tree of the parts contained in a message.
|
||||||
Return the number of parts of the mesage.
|
Return the number of parts of the mesage.
|
||||||
"""
|
"""
|
||||||
indent = depth * '\t'
|
indent = depth * "\t"
|
||||||
typ = part.get_content_type()
|
typ = part.get_content_type()
|
||||||
|
|
||||||
if part.is_multipart():
|
if part.is_multipart():
|
||||||
print(MelOutput.PART_MULTI_FORMAT.format(
|
print(
|
||||||
count=count, indent=indent, typ=typ))
|
MelOutput.PART_MULTI_FORMAT.format(count=count, indent=indent, typ=typ)
|
||||||
|
)
|
||||||
payl = part.get_payload()
|
payl = part.get_payload()
|
||||||
assert isinstance(payl, list)
|
assert isinstance(payl, list)
|
||||||
size = 1
|
size = 1
|
||||||
for obj in payl:
|
for obj in payl:
|
||||||
size += self.show_parts_tree(obj, depth=depth+1,
|
size += self.show_parts_tree(obj, depth=depth + 1, count=count + size)
|
||||||
count=count+size)
|
|
||||||
return size
|
return size
|
||||||
|
|
||||||
payl = part.get_payload(decode=True)
|
payl = part.get_payload(decode=True)
|
||||||
assert isinstance(payl, bytes)
|
assert isinstance(payl, bytes)
|
||||||
size = len(payl)
|
size = len(payl)
|
||||||
desc = part.get('Content-Description', '<no description>')
|
desc = part.get("Content-Description", "<no description>")
|
||||||
print(MelOutput.PART_LEAF_FORMAT.format(
|
print(
|
||||||
count=count, indent=indent, typ=typ, desc=desc,
|
MelOutput.PART_LEAF_FORMAT.format(
|
||||||
size=MelOutput.sizeof_fmt(size)))
|
count=count,
|
||||||
|
indent=indent,
|
||||||
|
typ=typ,
|
||||||
|
desc=desc,
|
||||||
|
size=MelOutput.sizeof_fmt(size),
|
||||||
|
)
|
||||||
|
)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
INTERESTING_HEADERS = ["Date", "From", "Subject", "To", "Cc", "Message-Id"]
|
INTERESTING_HEADERS = ["Date", "From", "Subject", "To", "Cc", "Message-Id"]
|
||||||
HEADER_FORMAT = colorama.Fore.BLUE + colorama.Style.BRIGHT + \
|
HEADER_FORMAT = (
|
||||||
'{}:' + colorama.Style.NORMAL + ' {}' + colorama.Style.RESET_ALL
|
colorama.Fore.BLUE
|
||||||
|
+ colorama.Style.BRIGHT
|
||||||
|
+ "{}:"
|
||||||
|
+ colorama.Style.NORMAL
|
||||||
|
+ " {}"
|
||||||
|
+ colorama.Style.RESET_ALL
|
||||||
|
)
|
||||||
|
|
||||||
def read_msg(self, msg: notmuch.Message) -> None:
|
def read_msg(self, msg: notmuch.Message) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -558,7 +612,7 @@ class MelOutput:
|
||||||
# Parse
|
# Parse
|
||||||
filename = msg.get_filename()
|
filename = msg.get_filename()
|
||||||
parser = email.parser.BytesParser()
|
parser = email.parser.BytesParser()
|
||||||
with open(filename, 'rb') as filedesc:
|
with open(filename, "rb") as filedesc:
|
||||||
mail = parser.parse(filedesc)
|
mail = parser.parse(filedesc)
|
||||||
|
|
||||||
# Defects
|
# Defects
|
||||||
|
@ -591,12 +645,13 @@ class MelOutput:
|
||||||
print(payl.decode())
|
print(payl.decode())
|
||||||
else:
|
else:
|
||||||
# TODO Use nametemplate from mailcap
|
# TODO Use nametemplate from mailcap
|
||||||
temp_file = '/tmp/melcap.html' # TODO Real temporary file
|
temp_file = "/tmp/melcap.html" # TODO Real temporary file
|
||||||
# TODO FIFO if possible
|
# TODO FIFO if possible
|
||||||
with open(temp_file, 'wb') as temp_filedesc:
|
with open(temp_file, "wb") as temp_filedesc:
|
||||||
temp_filedesc.write(payl)
|
temp_filedesc.write(payl)
|
||||||
command, _ = mailcap.findmatch(
|
command, _ = mailcap.findmatch(
|
||||||
self.caps, part.get_content_type(), key='view', filename=temp_file)
|
self.caps, part.get_content_type(), key="view", filename=temp_file
|
||||||
|
)
|
||||||
if command:
|
if command:
|
||||||
os.system(command)
|
os.system(command)
|
||||||
|
|
||||||
|
@ -611,21 +666,26 @@ class MelOutput:
|
||||||
line += arb[0].replace("'", "\\'")
|
line += arb[0].replace("'", "\\'")
|
||||||
line += colorama.Fore.LIGHTBLACK_EX
|
line += colorama.Fore.LIGHTBLACK_EX
|
||||||
for inter in arb[1:-1]:
|
for inter in arb[1:-1]:
|
||||||
line += '/' + inter.replace("'", "\\'")
|
line += "/" + inter.replace("'", "\\'")
|
||||||
line += '/' + colorama.Fore.WHITE + arb[-1].replace("'", "\\'")
|
line += "/" + colorama.Fore.WHITE + arb[-1].replace("'", "\\'")
|
||||||
line += colorama.Fore.LIGHTBLACK_EX + "'"
|
line += colorama.Fore.LIGHTBLACK_EX + "'"
|
||||||
line += colorama.Style.RESET_ALL
|
line += colorama.Style.RESET_ALL
|
||||||
print(line)
|
print(line)
|
||||||
|
|
||||||
|
|
||||||
class MelCLI():
|
class MelCLI:
|
||||||
"""
|
"""
|
||||||
Handles the user input and run asked operations.
|
Handles the user input and run asked operations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
VERBOSITY_LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "FATAL"]
|
VERBOSITY_LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "FATAL"]
|
||||||
|
|
||||||
def apply_msgs_input(self, argmessages: typing.List[str],
|
def apply_msgs_input(
|
||||||
action: typing.Callable, write: bool = False) -> None:
|
self,
|
||||||
|
argmessages: typing.List[str],
|
||||||
|
action: typing.Callable,
|
||||||
|
write: bool = False,
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Run a function on the message given by the user.
|
Run a function on the message given by the user.
|
||||||
"""
|
"""
|
||||||
|
@ -633,7 +693,7 @@ class MelCLI():
|
||||||
if not argmessages:
|
if not argmessages:
|
||||||
from_stdin = not sys.stdin.isatty()
|
from_stdin = not sys.stdin.isatty()
|
||||||
if argmessages:
|
if argmessages:
|
||||||
from_stdin = len(argmessages) == 1 and argmessages == '-'
|
from_stdin = len(argmessages) == 1 and argmessages == "-"
|
||||||
|
|
||||||
messages = list()
|
messages = list()
|
||||||
if from_stdin:
|
if from_stdin:
|
||||||
|
@ -646,9 +706,11 @@ class MelCLI():
|
||||||
else:
|
else:
|
||||||
for uids in argmessages:
|
for uids in argmessages:
|
||||||
if len(uids) > 12:
|
if len(uids) > 12:
|
||||||
self.log.warning("Might have forgotten some spaces "
|
self.log.warning(
|
||||||
"between the UIDs. Don't worry, I'll "
|
"Might have forgotten some spaces "
|
||||||
"split them for you")
|
"between the UIDs. Don't worry, I'll "
|
||||||
|
"split them for you"
|
||||||
|
)
|
||||||
for uid in MelOutput.chunks(uids, 12):
|
for uid in MelOutput.chunks(uids, 12):
|
||||||
if not MelEngine.is_uid(uid):
|
if not MelEngine.is_uid(uid):
|
||||||
self.log.error("Not an UID: %s", uid)
|
self.log.error("Not an UID: %s", uid)
|
||||||
|
@ -656,48 +718,52 @@ class MelCLI():
|
||||||
messages.append(uid)
|
messages.append(uid)
|
||||||
|
|
||||||
for message in messages:
|
for message in messages:
|
||||||
query_str = f'tag:tuid{message}'
|
query_str = f"tag:tuid{message}"
|
||||||
nb_msgs = self.engine.apply_msgs(query_str, action,
|
nb_msgs = self.engine.apply_msgs(
|
||||||
write=write, close_db=False)
|
query_str, action, write=write, close_db=False
|
||||||
|
)
|
||||||
if nb_msgs < 1:
|
if nb_msgs < 1:
|
||||||
self.log.error(
|
self.log.error("Couldn't execute function for message %s", message)
|
||||||
"Couldn't execute function for message %s", message)
|
|
||||||
self.engine.close_database()
|
self.engine.close_database()
|
||||||
|
|
||||||
def operation_default(self) -> None:
|
def operation_default(self) -> None:
|
||||||
"""
|
"""
|
||||||
Default operation: list all message in the inbox
|
Default operation: list all message in the inbox
|
||||||
"""
|
"""
|
||||||
self.engine.apply_msgs('tag:inbox', self.output.print_msg)
|
self.engine.apply_msgs("tag:inbox", self.output.print_msg)
|
||||||
|
|
||||||
def operation_inbox(self) -> None:
|
def operation_inbox(self) -> None:
|
||||||
"""
|
"""
|
||||||
Inbox operation: list all message in the inbox,
|
Inbox operation: list all message in the inbox,
|
||||||
possibly only the unread ones.
|
possibly only the unread ones.
|
||||||
"""
|
"""
|
||||||
query_str = 'tag:unread' if self.args.only_unread else 'tag:inbox'
|
query_str = "tag:unread" if self.args.only_unread else "tag:inbox"
|
||||||
self.engine.apply_msgs(query_str, self.output.print_msg)
|
self.engine.apply_msgs(query_str, self.output.print_msg)
|
||||||
|
|
||||||
def operation_flag(self) -> None:
|
def operation_flag(self) -> None:
|
||||||
"""
|
"""
|
||||||
Flag operation: Flag user selected messages.
|
Flag operation: Flag user selected messages.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def flag_msg(msg: notmuch.Message) -> None:
|
def flag_msg(msg: notmuch.Message) -> None:
|
||||||
"""
|
"""
|
||||||
Flag given message.
|
Flag given message.
|
||||||
"""
|
"""
|
||||||
msg.add_tag('flagged')
|
msg.add_tag("flagged")
|
||||||
|
|
||||||
self.apply_msgs_input(self.args.message, flag_msg, write=True)
|
self.apply_msgs_input(self.args.message, flag_msg, write=True)
|
||||||
|
|
||||||
def operation_unflag(self) -> None:
|
def operation_unflag(self) -> None:
|
||||||
"""
|
"""
|
||||||
Unflag operation: Flag user selected messages.
|
Unflag operation: Flag user selected messages.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def unflag_msg(msg: notmuch.Message) -> None:
|
def unflag_msg(msg: notmuch.Message) -> None:
|
||||||
"""
|
"""
|
||||||
Unflag given message.
|
Unflag given message.
|
||||||
"""
|
"""
|
||||||
msg.remove_tag('flagged')
|
msg.remove_tag("flagged")
|
||||||
|
|
||||||
self.apply_msgs_input(self.args.message, unflag_msg, write=True)
|
self.apply_msgs_input(self.args.message, unflag_msg, write=True)
|
||||||
|
|
||||||
def operation_read(self) -> None:
|
def operation_read(self) -> None:
|
||||||
|
@ -712,8 +778,7 @@ class MelCLI():
|
||||||
"""
|
"""
|
||||||
# Fetch mails
|
# Fetch mails
|
||||||
self.log.info("Fetching mails")
|
self.log.info("Fetching mails")
|
||||||
mbsync_config_file = os.path.expanduser(
|
mbsync_config_file = os.path.expanduser("~/.config/mbsyncrc") # TODO Better
|
||||||
"~/.config/mbsyncrc") # TODO Better
|
|
||||||
cmd = ["mbsync", "--config", mbsync_config_file, "--all"]
|
cmd = ["mbsync", "--config", mbsync_config_file, "--all"]
|
||||||
subprocess.run(cmd, check=False)
|
subprocess.run(cmd, check=False)
|
||||||
|
|
||||||
|
@ -724,8 +789,9 @@ class MelCLI():
|
||||||
self.output.notify_all()
|
self.output.notify_all()
|
||||||
|
|
||||||
# Tag new mails
|
# Tag new mails
|
||||||
self.engine.apply_msgs('tag:unprocessed', self.engine.retag_msg,
|
self.engine.apply_msgs(
|
||||||
show_progress=True, write=True)
|
"tag:unprocessed", self.engine.retag_msg, show_progress=True, write=True
|
||||||
|
)
|
||||||
|
|
||||||
def operation_list(self) -> None:
|
def operation_list(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -744,14 +810,15 @@ class MelCLI():
|
||||||
Retag operation: Manually retag all the mails in the database.
|
Retag operation: Manually retag all the mails in the database.
|
||||||
Mostly debug I suppose.
|
Mostly debug I suppose.
|
||||||
"""
|
"""
|
||||||
self.engine.apply_msgs('*', self.engine.retag_msg,
|
self.engine.apply_msgs(
|
||||||
show_progress=True, write=True)
|
"*", self.engine.retag_msg, show_progress=True, write=True
|
||||||
|
)
|
||||||
|
|
||||||
def operation_all(self) -> None:
|
def operation_all(self) -> None:
|
||||||
"""
|
"""
|
||||||
All operation: list every single message.
|
All operation: list every single message.
|
||||||
"""
|
"""
|
||||||
self.engine.apply_msgs('*', self.output.print_msg)
|
self.engine.apply_msgs("*", self.output.print_msg)
|
||||||
|
|
||||||
def add_subparsers(self) -> None:
|
def add_subparsers(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -766,29 +833,30 @@ class MelCLI():
|
||||||
|
|
||||||
# inbox (default)
|
# inbox (default)
|
||||||
parser_inbox = subparsers.add_parser(
|
parser_inbox = subparsers.add_parser(
|
||||||
"inbox", help="Show unread, unsorted and flagged messages")
|
"inbox", help="Show unread, unsorted and flagged messages"
|
||||||
parser_inbox.add_argument('-u', '--only-unread', action='store_true',
|
)
|
||||||
help="Show unread messages only")
|
parser_inbox.add_argument(
|
||||||
|
"-u", "--only-unread", action="store_true", help="Show unread messages only"
|
||||||
|
)
|
||||||
# TODO Make this more relevant
|
# TODO Make this more relevant
|
||||||
parser_inbox.set_defaults(operation=self.operation_inbox)
|
parser_inbox.set_defaults(operation=self.operation_inbox)
|
||||||
|
|
||||||
# list folder [--recurse]
|
# list folder [--recurse]
|
||||||
# List actions
|
# List actions
|
||||||
parser_list = subparsers.add_parser(
|
parser_list = subparsers.add_parser("list", help="List all folders")
|
||||||
"list", help="List all folders")
|
|
||||||
# parser_list.add_argument('message', nargs='*', help="Messages")
|
# parser_list.add_argument('message', nargs='*', help="Messages")
|
||||||
parser_list.set_defaults(operation=self.operation_list)
|
parser_list.set_defaults(operation=self.operation_list)
|
||||||
|
|
||||||
# flag msg...
|
# flag msg...
|
||||||
parser_flag = subparsers.add_parser(
|
parser_flag = subparsers.add_parser("flag", help="Mark messages as flagged")
|
||||||
"flag", help="Mark messages as flagged")
|
parser_flag.add_argument("message", nargs="*", help="Messages")
|
||||||
parser_flag.add_argument('message', nargs='*', help="Messages")
|
|
||||||
parser_flag.set_defaults(operation=self.operation_flag)
|
parser_flag.set_defaults(operation=self.operation_flag)
|
||||||
|
|
||||||
# unflag msg...
|
# unflag msg...
|
||||||
parser_unflag = subparsers.add_parser(
|
parser_unflag = subparsers.add_parser(
|
||||||
"unflag", help="Mark messages as not-flagged")
|
"unflag", help="Mark messages as not-flagged"
|
||||||
parser_unflag.add_argument('message', nargs='*', help="Messages")
|
)
|
||||||
|
parser_unflag.add_argument("message", nargs="*", help="Messages")
|
||||||
parser_unflag.set_defaults(operation=self.operation_unflag)
|
parser_unflag.set_defaults(operation=self.operation_unflag)
|
||||||
|
|
||||||
# delete msg...
|
# delete msg...
|
||||||
|
@ -799,7 +867,7 @@ class MelCLI():
|
||||||
# read msg [--html] [--plain] [--browser]
|
# read msg [--html] [--plain] [--browser]
|
||||||
|
|
||||||
parser_read = subparsers.add_parser("read", help="Read message")
|
parser_read = subparsers.add_parser("read", help="Read message")
|
||||||
parser_read.add_argument('message', nargs=1, help="Messages")
|
parser_read.add_argument("message", nargs=1, help="Messages")
|
||||||
parser_read.set_defaults(operation=self.operation_read)
|
parser_read.set_defaults(operation=self.operation_read)
|
||||||
|
|
||||||
# attach msg [id] [--save] (list if no id, xdg-open else)
|
# attach msg [id] [--save] (list if no id, xdg-open else)
|
||||||
|
@ -817,20 +885,23 @@ class MelCLI():
|
||||||
# fetch (mbsync, notmuch new, retag, notify; called by greater gods)
|
# fetch (mbsync, notmuch new, retag, notify; called by greater gods)
|
||||||
|
|
||||||
parser_fetch = subparsers.add_parser(
|
parser_fetch = subparsers.add_parser(
|
||||||
"fetch", help="Fetch mail, tag them, and run notifications")
|
"fetch", help="Fetch mail, tag them, and run notifications"
|
||||||
|
)
|
||||||
parser_fetch.set_defaults(operation=self.operation_fetch)
|
parser_fetch.set_defaults(operation=self.operation_fetch)
|
||||||
|
|
||||||
# Debug
|
# Debug
|
||||||
|
|
||||||
# debug (various)
|
# debug (various)
|
||||||
parser_debug = subparsers.add_parser(
|
parser_debug = subparsers.add_parser(
|
||||||
"debug", help="Who know what this holds...")
|
"debug", help="Who know what this holds..."
|
||||||
parser_debug.set_defaults(verbosity='DEBUG')
|
)
|
||||||
|
parser_debug.set_defaults(verbosity="DEBUG")
|
||||||
parser_debug.set_defaults(operation=self.operation_debug)
|
parser_debug.set_defaults(operation=self.operation_debug)
|
||||||
|
|
||||||
# retag (all or unprocessed)
|
# retag (all or unprocessed)
|
||||||
parser_retag = subparsers.add_parser(
|
parser_retag = subparsers.add_parser(
|
||||||
"retag", help="Retag all mails (when you changed configuration)")
|
"retag", help="Retag all mails (when you changed configuration)"
|
||||||
|
)
|
||||||
parser_retag.set_defaults(operation=self.operation_retag)
|
parser_retag.set_defaults(operation=self.operation_retag)
|
||||||
|
|
||||||
# all
|
# all
|
||||||
|
@ -842,15 +913,21 @@ class MelCLI():
|
||||||
Create the main parser that will handle the user arguments.
|
Create the main parser that will handle the user arguments.
|
||||||
"""
|
"""
|
||||||
parser = argparse.ArgumentParser(description="Meh mail client")
|
parser = argparse.ArgumentParser(description="Meh mail client")
|
||||||
parser.add_argument('-v', '--verbosity',
|
parser.add_argument(
|
||||||
choices=MelCLI.VERBOSITY_LEVELS, default='WARNING',
|
"-v",
|
||||||
help="Verbosity of self.log messages")
|
"--verbosity",
|
||||||
|
choices=MelCLI.VERBOSITY_LEVELS,
|
||||||
|
default="WARNING",
|
||||||
|
help="Verbosity of self.log messages",
|
||||||
|
)
|
||||||
# parser.add_argument('-n', '--dry-run', action='store_true',
|
# parser.add_argument('-n', '--dry-run', action='store_true',
|
||||||
# help="Don't do anything") # DEBUG
|
# help="Don't do anything") # DEBUG
|
||||||
default_config_file = os.path.join(
|
default_config_file = os.path.join(
|
||||||
xdg.BaseDirectory.xdg_config_home, 'mel', 'accounts.conf')
|
xdg.BaseDirectory.xdg_config_home, "mel", "accounts.conf"
|
||||||
parser.add_argument('-c', '--config', default=default_config_file,
|
)
|
||||||
help="Accounts config file")
|
parser.add_argument(
|
||||||
|
"-c", "--config", default=default_config_file, help="Accounts config file"
|
||||||
|
)
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
@ -860,8 +937,9 @@ class MelCLI():
|
||||||
self.add_subparsers()
|
self.add_subparsers()
|
||||||
|
|
||||||
self.args = self.parser.parse_args()
|
self.args = self.parser.parse_args()
|
||||||
coloredlogs.install(level=self.args.verbosity,
|
coloredlogs.install(
|
||||||
fmt='%(levelname)s %(name)s %(message)s')
|
level=self.args.verbosity, fmt="%(levelname)s %(name)s %(message)s"
|
||||||
|
)
|
||||||
|
|
||||||
self.engine = MelEngine(self.args.config)
|
self.engine = MelEngine(self.args.config)
|
||||||
self.output = MelOutput(self.engine)
|
self.output = MelOutput(self.engine)
|
||||||
|
|
|
@ -14,7 +14,7 @@ import sys
|
||||||
# TODO Write in .config or .cache /mel
|
# TODO Write in .config or .cache /mel
|
||||||
# TODO Fix IMAPS with mbsync
|
# TODO Fix IMAPS with mbsync
|
||||||
|
|
||||||
configPath = os.path.join(os.path.expanduser('~'), '.config', 'mel', 'accounts.conf')
|
configPath = os.path.join(os.path.expanduser("~"), ".config", "mel", "accounts.conf")
|
||||||
|
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
config.read(configPath)
|
config.read(configPath)
|
||||||
|
@ -23,9 +23,9 @@ storageFull = os.path.realpath(os.path.expanduser(config["GENERAL"]["storage"]))
|
||||||
config["GENERAL"]["storage"] = storageFull
|
config["GENERAL"]["storage"] = storageFull
|
||||||
|
|
||||||
SERVER_DEFAULTS = {
|
SERVER_DEFAULTS = {
|
||||||
"imap": {"port": 143, "starttls": True},
|
"imap": {"port": 143, "starttls": True},
|
||||||
"smtp": {"port": 587, "starttls": True},
|
"smtp": {"port": 587, "starttls": True},
|
||||||
}
|
}
|
||||||
SERVER_ITEMS = {"host", "port", "user", "pass", "starttls"}
|
SERVER_ITEMS = {"host", "port", "user", "pass", "starttls"}
|
||||||
ACCOUNT_DEFAULTS = {
|
ACCOUNT_DEFAULTS = {
|
||||||
"color": "#FFFFFF",
|
"color": "#FFFFFF",
|
||||||
|
@ -53,7 +53,11 @@ for name in config.sections():
|
||||||
for item in SERVER_ITEMS:
|
for item in SERVER_ITEMS:
|
||||||
key = server + item
|
key = server + item
|
||||||
try:
|
try:
|
||||||
val = section.get(key) or section.get(item) or SERVER_DEFAULTS[server][item]
|
val = (
|
||||||
|
section.get(key)
|
||||||
|
or section.get(item)
|
||||||
|
or SERVER_DEFAULTS[server][item]
|
||||||
|
)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise KeyError("{}.{}".format(name, key))
|
raise KeyError("{}.{}".format(name, key))
|
||||||
|
|
||||||
|
@ -71,7 +75,7 @@ for name in config.sections():
|
||||||
continue
|
continue
|
||||||
data[key] = section[key]
|
data[key] = section[key]
|
||||||
|
|
||||||
for k, v in config['DEFAULT'].items():
|
for k, v in config["DEFAULT"].items():
|
||||||
if k not in data:
|
if k not in data:
|
||||||
data[k] = v
|
data[k] = v
|
||||||
|
|
||||||
|
@ -85,7 +89,7 @@ for name in config.sections():
|
||||||
mails.add(alt)
|
mails.add(alt)
|
||||||
|
|
||||||
data["account"] = name
|
data["account"] = name
|
||||||
data["storage"] = os.path.join(config['GENERAL']['storage'], name)
|
data["storage"] = os.path.join(config["GENERAL"]["storage"], name)
|
||||||
data["storageInbox"] = os.path.join(data["storage"], "INBOX")
|
data["storageInbox"] = os.path.join(data["storage"], "INBOX")
|
||||||
accounts[name] = data
|
accounts[name] = data
|
||||||
|
|
||||||
|
@ -139,7 +143,7 @@ remotepass = {imappass}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
offlineIMAPstr = OFFLINEIMAP_BEGIN.format(','.join(accounts), len(accounts))
|
offlineIMAPstr = OFFLINEIMAP_BEGIN.format(",".join(accounts), len(accounts))
|
||||||
for name, account in accounts.items():
|
for name, account in accounts.items():
|
||||||
if account["imapstarttls"]:
|
if account["imapstarttls"]:
|
||||||
secconf = "ssl = no"
|
secconf = "ssl = no"
|
||||||
|
@ -182,9 +186,11 @@ for name, account in accounts.items():
|
||||||
if "certificate" in account:
|
if "certificate" in account:
|
||||||
secconf += "\nCertificateFile {certificate}".format(**account)
|
secconf += "\nCertificateFile {certificate}".format(**account)
|
||||||
imappassEscaped = account["imappass"].replace("\\", "\\\\")
|
imappassEscaped = account["imappass"].replace("\\", "\\\\")
|
||||||
mbsyncStr += MBSYNC_ACCOUNT.format(**account, secconf=secconf, imappassEscaped=imappassEscaped)
|
mbsyncStr += MBSYNC_ACCOUNT.format(
|
||||||
mbsyncFilepath = os.path.join(os.path.expanduser('~'), '.config/mel/mbsyncrc')
|
**account, secconf=secconf, imappassEscaped=imappassEscaped
|
||||||
with open(mbsyncFilepath, 'w') as f:
|
)
|
||||||
|
mbsyncFilepath = os.path.join(os.path.expanduser("~"), ".config/mel/mbsyncrc")
|
||||||
|
with open(mbsyncFilepath, "w") as f:
|
||||||
f.write(mbsyncStr)
|
f.write(mbsyncStr)
|
||||||
|
|
||||||
# msmtp
|
# msmtp
|
||||||
|
@ -208,8 +214,8 @@ tls on
|
||||||
msmtpStr = MSMTP_BEGIN
|
msmtpStr = MSMTP_BEGIN
|
||||||
for name, account in accounts.items():
|
for name, account in accounts.items():
|
||||||
msmtpStr += MSMTP_ACCOUNT.format(**account)
|
msmtpStr += MSMTP_ACCOUNT.format(**account)
|
||||||
mbsyncFilepath = os.path.join(os.path.expanduser('~'), '.config/msmtp/config')
|
mbsyncFilepath = os.path.join(os.path.expanduser("~"), ".config/msmtp/config")
|
||||||
with open(mbsyncFilepath, 'w') as f:
|
with open(mbsyncFilepath, "w") as f:
|
||||||
f.write(msmtpStr)
|
f.write(msmtpStr)
|
||||||
|
|
||||||
|
|
||||||
|
@ -241,8 +247,8 @@ other_email = mails.copy()
|
||||||
other_email.remove(general["main"]["from"])
|
other_email.remove(general["main"]["from"])
|
||||||
other_email = ";".join(other_email)
|
other_email = ";".join(other_email)
|
||||||
notmuchStr = NOTMUCH_BEGIN.format(**general, other_email=other_email)
|
notmuchStr = NOTMUCH_BEGIN.format(**general, other_email=other_email)
|
||||||
mbsyncFilepath = os.path.join(os.path.expanduser('~'), '.config/notmuch-config')
|
mbsyncFilepath = os.path.join(os.path.expanduser("~"), ".config/notmuch-config")
|
||||||
with open(mbsyncFilepath, 'w') as f:
|
with open(mbsyncFilepath, "w") as f:
|
||||||
f.write(notmuchStr)
|
f.write(notmuchStr)
|
||||||
|
|
||||||
# mutt (temp)
|
# mutt (temp)
|
||||||
|
@ -254,15 +260,15 @@ mailboxesStr = MAILBOXES_BEGIN
|
||||||
for name, account in accounts.items():
|
for name, account in accounts.items():
|
||||||
lines = "-" * (20 - len(name))
|
lines = "-" * (20 - len(name))
|
||||||
mailboxesStr += f' "+{name}{lines}"'
|
mailboxesStr += f' "+{name}{lines}"'
|
||||||
for root, dirs, files in os.walk(account['storage']):
|
for root, dirs, files in os.walk(account["storage"]):
|
||||||
if "cur" not in dirs or "new" not in dirs or "tmp" not in dirs:
|
if "cur" not in dirs or "new" not in dirs or "tmp" not in dirs:
|
||||||
continue
|
continue
|
||||||
assert root.startswith(storageFull)
|
assert root.startswith(storageFull)
|
||||||
path = root[len(storageFull)+1:]
|
path = root[len(storageFull) + 1 :]
|
||||||
mailboxesStr += f' "+{path}"'
|
mailboxesStr += f' "+{path}"'
|
||||||
mailboxesStr += "\n"
|
mailboxesStr += "\n"
|
||||||
mailboxesFilepath = os.path.join(os.path.expanduser('~'), '.mutt/mailboxes')
|
mailboxesFilepath = os.path.join(os.path.expanduser("~"), ".mutt/mailboxes")
|
||||||
with open(mailboxesFilepath, 'w') as f:
|
with open(mailboxesFilepath, "w") as f:
|
||||||
f.write(mailboxesStr)
|
f.write(mailboxesStr)
|
||||||
|
|
||||||
## accounts
|
## accounts
|
||||||
|
@ -296,14 +302,14 @@ for name, account in accounts.items():
|
||||||
muttStr = MUTT_ACCOUNT.format(**account)
|
muttStr = MUTT_ACCOUNT.format(**account)
|
||||||
|
|
||||||
# Config
|
# Config
|
||||||
muttFilepath = os.path.join(os.path.expanduser('~'), f'.mutt/accounts/{name}')
|
muttFilepath = os.path.join(os.path.expanduser("~"), f".mutt/accounts/{name}")
|
||||||
with open(muttFilepath, 'w') as f:
|
with open(muttFilepath, "w") as f:
|
||||||
f.write(muttStr)
|
f.write(muttStr)
|
||||||
|
|
||||||
# Signature
|
# Signature
|
||||||
sigStr = account.get("sig", account.get("name", ""))
|
sigStr = account.get("sig", account.get("name", ""))
|
||||||
sigFilepath = os.path.join(os.path.expanduser('~'), f'.mutt/accounts/{name}.sig')
|
sigFilepath = os.path.join(os.path.expanduser("~"), f".mutt/accounts/{name}.sig")
|
||||||
with open(sigFilepath, 'w') as f:
|
with open(sigFilepath, "w") as f:
|
||||||
f.write(sigStr)
|
f.write(sigStr)
|
||||||
|
|
||||||
MUTT_SELECTOR = """
|
MUTT_SELECTOR = """
|
||||||
|
@ -324,13 +330,15 @@ hooks = ""
|
||||||
for name, account in accounts.items():
|
for name, account in accounts.items():
|
||||||
hooks += f"folder-hook {name}/* source ~/.mutt/accounts/{name}\n"
|
hooks += f"folder-hook {name}/* source ~/.mutt/accounts/{name}\n"
|
||||||
selectStr += MUTT_SELECTOR.format(**general, hooks=hooks)
|
selectStr += MUTT_SELECTOR.format(**general, hooks=hooks)
|
||||||
selectFilepath = os.path.join(os.path.expanduser('~'), '.mutt/muttrc')
|
selectFilepath = os.path.join(os.path.expanduser("~"), ".mutt/muttrc")
|
||||||
with open(selectFilepath, 'w') as f:
|
with open(selectFilepath, "w") as f:
|
||||||
f.write(selectStr)
|
f.write(selectStr)
|
||||||
|
|
||||||
## Color
|
## Color
|
||||||
for name, account in accounts.items():
|
for name, account in accounts.items():
|
||||||
# Config
|
# Config
|
||||||
colorFilepath = os.path.join(os.path.expanduser('~'), f'{general["storage"]}/{name}/color')
|
colorFilepath = os.path.join(
|
||||||
with open(colorFilepath, 'w') as f:
|
os.path.expanduser("~"), f'{general["storage"]}/{name}/color'
|
||||||
f.write(account['color'])
|
)
|
||||||
|
with open(colorFilepath, "w") as f:
|
||||||
|
f.write(account["color"])
|
||||||
|
|
|
@ -16,17 +16,17 @@ def main() -> None:
|
||||||
"""
|
"""
|
||||||
Function that executes the script.
|
Function that executes the script.
|
||||||
"""
|
"""
|
||||||
for root, _, files in os.walk('.'):
|
for root, _, files in os.walk("."):
|
||||||
for filename in files:
|
for filename in files:
|
||||||
match = re.match(r'^(\d+) - (.+)$', filename)
|
match = re.match(r"^(\d+) - (.+)$", filename)
|
||||||
if not match:
|
if not match:
|
||||||
continue
|
continue
|
||||||
new_filename = f"{match[1]} {match[2]}"
|
new_filename = f"{match[1]} {match[2]}"
|
||||||
old_path = os.path.join(root, filename)
|
old_path = os.path.join(root, filename)
|
||||||
new_path = os.path.join(root, new_filename)
|
new_path = os.path.join(root, new_filename)
|
||||||
print(old_path, '->', new_path)
|
print(old_path, "->", new_path)
|
||||||
os.rename(old_path, new_path)
|
os.rename(old_path, new_path)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -6,7 +6,7 @@ import shutil
|
||||||
import logging
|
import logging
|
||||||
import coloredlogs
|
import coloredlogs
|
||||||
|
|
||||||
coloredlogs.install(level='DEBUG', fmt='%(levelname)s %(message)s')
|
coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s")
|
||||||
log = logging.getLogger()
|
log = logging.getLogger()
|
||||||
|
|
||||||
MUSICS_FOLDER = os.path.join(os.path.expanduser("~"), "Musique")
|
MUSICS_FOLDER = os.path.join(os.path.expanduser("~"), "Musique")
|
||||||
|
@ -36,4 +36,3 @@ for f in sys.argv[1:]:
|
||||||
log.info("{} → {}".format(src, dst))
|
log.info("{} → {}".format(src, dst))
|
||||||
os.makedirs(dstFolder, exist_ok=True)
|
os.makedirs(dstFolder, exist_ok=True)
|
||||||
shutil.move(src, dst)
|
shutil.move(src, dst)
|
||||||
|
|
||||||
|
|
|
@ -10,15 +10,17 @@ import logging
|
||||||
import coloredlogs
|
import coloredlogs
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
coloredlogs.install(level='DEBUG', fmt='%(levelname)s %(message)s')
|
coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s")
|
||||||
log = logging.getLogger()
|
log = logging.getLogger()
|
||||||
|
|
||||||
debug = None
|
debug = None
|
||||||
|
|
||||||
class OvhCli():
|
|
||||||
|
class OvhCli:
|
||||||
ROOT = "https://api.ovh.com/1.0?null"
|
ROOT = "https://api.ovh.com/1.0?null"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.cacheDir = os.path.join(xdg.BaseDirectory.xdg_cache_home, 'ovhcli')
|
self.cacheDir = os.path.join(xdg.BaseDirectory.xdg_cache_home, "ovhcli")
|
||||||
# TODO Corner cases: links, cache dir not done, configurable cache
|
# TODO Corner cases: links, cache dir not done, configurable cache
|
||||||
if not os.path.isdir(self.cacheDir):
|
if not os.path.isdir(self.cacheDir):
|
||||||
assert not os.path.exists(self.cacheDir)
|
assert not os.path.exists(self.cacheDir)
|
||||||
|
@ -26,18 +28,18 @@ class OvhCli():
|
||||||
|
|
||||||
def updateCache(self):
|
def updateCache(self):
|
||||||
log.info("Downloading the API description")
|
log.info("Downloading the API description")
|
||||||
rootJsonPath = os.path.join(self.cacheDir, 'root.json')
|
rootJsonPath = os.path.join(self.cacheDir, "root.json")
|
||||||
log.debug(f"{self.ROOT} -> {rootJsonPath}")
|
log.debug(f"{self.ROOT} -> {rootJsonPath}")
|
||||||
urllib.request.urlretrieve(self.ROOT, rootJsonPath)
|
urllib.request.urlretrieve(self.ROOT, rootJsonPath)
|
||||||
with open(rootJsonPath, 'rt') as rootJson:
|
with open(rootJsonPath, "rt") as rootJson:
|
||||||
root = json.load(rootJson)
|
root = json.load(rootJson)
|
||||||
basePath = root['basePath']
|
basePath = root["basePath"]
|
||||||
|
|
||||||
for apiRoot in root['apis']:
|
for apiRoot in root["apis"]:
|
||||||
fmt = 'json'
|
fmt = "json"
|
||||||
assert fmt in apiRoot['format']
|
assert fmt in apiRoot["format"]
|
||||||
path = apiRoot['path']
|
path = apiRoot["path"]
|
||||||
schema = apiRoot['schema'].format(format=fmt, path=path)
|
schema = apiRoot["schema"].format(format=fmt, path=path)
|
||||||
apiJsonPath = os.path.join(self.cacheDir, schema[1:])
|
apiJsonPath = os.path.join(self.cacheDir, schema[1:])
|
||||||
apiJsonUrl = basePath + schema
|
apiJsonUrl = basePath + schema
|
||||||
log.debug(f"{apiJsonUrl} -> {apiJsonPath}")
|
log.debug(f"{apiJsonUrl} -> {apiJsonPath}")
|
||||||
|
@ -47,11 +49,11 @@ class OvhCli():
|
||||||
urllib.request.urlretrieve(apiJsonUrl, apiJsonPath)
|
urllib.request.urlretrieve(apiJsonUrl, apiJsonPath)
|
||||||
|
|
||||||
def createParser(self):
|
def createParser(self):
|
||||||
parser = argparse.ArgumentParser(description='Access the OVH API')
|
parser = argparse.ArgumentParser(description="Access the OVH API")
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
cli = OvhCli()
|
cli = OvhCli()
|
||||||
# cli.updateCache()
|
# cli.updateCache()
|
||||||
parser = cli.createParser()
|
parser = cli.createParser()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
@ -9,16 +9,16 @@ import PIL.ExifTags
|
||||||
import PIL.Image
|
import PIL.Image
|
||||||
import progressbar
|
import progressbar
|
||||||
|
|
||||||
EXTENSION_PATTERN = re.compile(r'\.JPE?G', re.I)
|
EXTENSION_PATTERN = re.compile(r"\.JPE?G", re.I)
|
||||||
COMMON_PATTERN = re.compile(r'(IMG|DSC[NF]?|100|P10|f|t)_?\d+', re.I)
|
COMMON_PATTERN = re.compile(r"(IMG|DSC[NF]?|100|P10|f|t)_?\d+", re.I)
|
||||||
EXIF_TAG_NAME = 'DateTimeOriginal'
|
EXIF_TAG_NAME = "DateTimeOriginal"
|
||||||
EXIF_TAG_ID = list(PIL.ExifTags.TAGS.keys())[list(
|
EXIF_TAG_ID = list(PIL.ExifTags.TAGS.keys())[
|
||||||
PIL.ExifTags.TAGS.values()).index(EXIF_TAG_NAME)]
|
list(PIL.ExifTags.TAGS.values()).index(EXIF_TAG_NAME)
|
||||||
EXIF_DATE_FORMAT = '%Y:%m:%d %H:%M:%S'
|
]
|
||||||
|
EXIF_DATE_FORMAT = "%Y:%m:%d %H:%M:%S"
|
||||||
|
|
||||||
|
|
||||||
def get_pictures(directory: str = ".", skip_renamed: bool = True) \
|
def get_pictures(directory: str = ".", skip_renamed: bool = True) -> typing.Generator:
|
||||||
-> typing.Generator:
|
|
||||||
for root, _, files in os.walk(directory):
|
for root, _, files in os.walk(directory):
|
||||||
for filename in files:
|
for filename in files:
|
||||||
filename_trunk, extension = os.path.splitext(filename)
|
filename_trunk, extension = os.path.splitext(filename)
|
||||||
|
@ -44,9 +44,9 @@ def main() -> None:
|
||||||
if exif_data and EXIF_TAG_ID in exif_data:
|
if exif_data and EXIF_TAG_ID in exif_data:
|
||||||
date_raw = exif_data[EXIF_TAG_ID]
|
date_raw = exif_data[EXIF_TAG_ID]
|
||||||
date = datetime.datetime.strptime(date_raw, EXIF_DATE_FORMAT)
|
date = datetime.datetime.strptime(date_raw, EXIF_DATE_FORMAT)
|
||||||
new_name = date.isoformat().replace(':', '-') + '.jpg' # For NTFS
|
new_name = date.isoformat().replace(":", "-") + ".jpg" # For NTFS
|
||||||
print(full_path, new_name)
|
print(full_path, new_name)
|
||||||
os.rename(full_path, new_name) # TODO FOLDER
|
os.rename(full_path, new_name) # TODO FOLDER
|
||||||
img.close()
|
img.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -9,14 +9,16 @@ from Xlib.protocol import rq
|
||||||
|
|
||||||
KEY = XK.XK_F7
|
KEY = XK.XK_F7
|
||||||
|
|
||||||
|
|
||||||
def mute(state):
|
def mute(state):
|
||||||
with pulsectl.Pulse('list-source') as pulse:
|
with pulsectl.Pulse("list-source") as pulse:
|
||||||
for source in pulse.source_list():
|
for source in pulse.source_list():
|
||||||
if source.port_active:
|
if source.port_active:
|
||||||
if source.mute != state:
|
if source.mute != state:
|
||||||
pulse.mute(source, state)
|
pulse.mute(source, state)
|
||||||
print(f"{source.name} {'un' if not state else ''}muted")
|
print(f"{source.name} {'un' if not state else ''}muted")
|
||||||
|
|
||||||
|
|
||||||
mute(True)
|
mute(True)
|
||||||
|
|
||||||
local_dpy = display.Display()
|
local_dpy = display.Display()
|
||||||
|
@ -36,7 +38,8 @@ def record_callback(reply):
|
||||||
data = reply.data
|
data = reply.data
|
||||||
while len(data):
|
while len(data):
|
||||||
event, data = rq.EventField(None).parse_binary_value(
|
event, data = rq.EventField(None).parse_binary_value(
|
||||||
data, record_dpy.display, None, None)
|
data, record_dpy.display, None, None
|
||||||
|
)
|
||||||
|
|
||||||
if event.type in [X.KeyPress, X.KeyRelease]:
|
if event.type in [X.KeyPress, X.KeyRelease]:
|
||||||
keysym = local_dpy.keycode_to_keysym(event.detail, 0)
|
keysym = local_dpy.keycode_to_keysym(event.detail, 0)
|
||||||
|
@ -45,29 +48,32 @@ def record_callback(reply):
|
||||||
if keysym == KEY:
|
if keysym == KEY:
|
||||||
mute(event.type == X.KeyRelease)
|
mute(event.type == X.KeyRelease)
|
||||||
|
|
||||||
|
|
||||||
# Check if the extension is present
|
# Check if the extension is present
|
||||||
if not record_dpy.has_extension("RECORD"):
|
if not record_dpy.has_extension("RECORD"):
|
||||||
print("RECORD extension not found")
|
print("RECORD extension not found")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
r = record_dpy.record_get_version(0, 0)
|
r = record_dpy.record_get_version(0, 0)
|
||||||
print("RECORD extension version %d.%d" %
|
print("RECORD extension version %d.%d" % (r.major_version, r.minor_version))
|
||||||
(r.major_version, r.minor_version))
|
|
||||||
|
|
||||||
# Create a recording context; we only want key and mouse events
|
# Create a recording context; we only want key and mouse events
|
||||||
ctx = record_dpy.record_create_context(
|
ctx = record_dpy.record_create_context(
|
||||||
0,
|
0,
|
||||||
[record.AllClients],
|
[record.AllClients],
|
||||||
[{
|
[
|
||||||
'core_requests': (0, 0),
|
{
|
||||||
'core_replies': (0, 0),
|
"core_requests": (0, 0),
|
||||||
'ext_requests': (0, 0, 0, 0),
|
"core_replies": (0, 0),
|
||||||
'ext_replies': (0, 0, 0, 0),
|
"ext_requests": (0, 0, 0, 0),
|
||||||
'delivered_events': (0, 0),
|
"ext_replies": (0, 0, 0, 0),
|
||||||
'device_events': (X.KeyPress, X.MotionNotify),
|
"delivered_events": (0, 0),
|
||||||
'errors': (0, 0),
|
"device_events": (X.KeyPress, X.MotionNotify),
|
||||||
'client_started': False,
|
"errors": (0, 0),
|
||||||
'client_died': False,
|
"client_started": False,
|
||||||
}])
|
"client_died": False,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
# Enable the context; this only returns after a call to record_disable_context,
|
# Enable the context; this only returns after a call to record_disable_context,
|
||||||
# while calling the callback function in the meantime
|
# while calling the callback function in the meantime
|
||||||
|
|
|
@ -14,15 +14,15 @@ import typing
|
||||||
import coloredlogs
|
import coloredlogs
|
||||||
import r128gain
|
import r128gain
|
||||||
|
|
||||||
coloredlogs.install(level='DEBUG', fmt='%(levelname)s %(message)s')
|
coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s")
|
||||||
log = logging.getLogger()
|
log = logging.getLogger()
|
||||||
|
|
||||||
# TODO Remove debug
|
# TODO Remove debug
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
FORCE = '-f' in sys.argv
|
FORCE = "-f" in sys.argv
|
||||||
if FORCE:
|
if FORCE:
|
||||||
sys.argv.remove('-f')
|
sys.argv.remove("-f")
|
||||||
if len(sys.argv) >= 2:
|
if len(sys.argv) >= 2:
|
||||||
SOURCE_FOLDER = os.path.realpath(sys.argv[1])
|
SOURCE_FOLDER = os.path.realpath(sys.argv[1])
|
||||||
else:
|
else:
|
||||||
|
@ -66,6 +66,5 @@ for album in albums:
|
||||||
if not musicFiles:
|
if not musicFiles:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
r128gain.process(musicFiles, album_gain=True,
|
r128gain.process(musicFiles, album_gain=True, skip_tagged=not FORCE, report=True)
|
||||||
skip_tagged=not FORCE, report=True)
|
|
||||||
print("==============================")
|
print("==============================")
|
||||||
|
|
|
@ -13,7 +13,7 @@ import progressbar
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
progressbar.streams.wrap_stderr()
|
progressbar.streams.wrap_stderr()
|
||||||
coloredlogs.install(level='INFO', fmt='%(levelname)s %(message)s')
|
coloredlogs.install(level="INFO", fmt="%(levelname)s %(message)s")
|
||||||
log = logging.getLogger()
|
log = logging.getLogger()
|
||||||
|
|
||||||
# 1) Create file list with conflict files
|
# 1) Create file list with conflict files
|
||||||
|
@ -21,21 +21,20 @@ log = logging.getLogger()
|
||||||
# 3) Propose what to do
|
# 3) Propose what to do
|
||||||
|
|
||||||
|
|
||||||
def sizeof_fmt(num, suffix='B'):
|
def sizeof_fmt(num, suffix="B"):
|
||||||
# Stolen from https://stackoverflow.com/a/1094933
|
# Stolen from https://stackoverflow.com/a/1094933
|
||||||
for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
|
for unit in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]:
|
||||||
if abs(num) < 1024.0:
|
if abs(num) < 1024.0:
|
||||||
return "%3.1f %s%s" % (num, unit, suffix)
|
return "%3.1f %s%s" % (num, unit, suffix)
|
||||||
num /= 1024.0
|
num /= 1024.0
|
||||||
return "%.1f %s%s" % (num, 'Yi', suffix)
|
return "%.1f %s%s" % (num, "Yi", suffix)
|
||||||
|
|
||||||
|
|
||||||
class Table():
|
class Table:
|
||||||
def __init__(self, width, height):
|
def __init__(self, width, height):
|
||||||
self.width = width
|
self.width = width
|
||||||
self.height = height
|
self.height = height
|
||||||
self.data = [['' for _ in range(self.height)]
|
self.data = [["" for _ in range(self.height)] for _ in range(self.width)]
|
||||||
for _ in range(self.width)]
|
|
||||||
|
|
||||||
def set(self, x, y, data):
|
def set(self, x, y, data):
|
||||||
self.data[x][y] = str(data)
|
self.data[x][y] = str(data)
|
||||||
|
@ -48,15 +47,15 @@ class Table():
|
||||||
l = len(cell)
|
l = len(cell)
|
||||||
width = widths[x]
|
width = widths[x]
|
||||||
if x > 0:
|
if x > 0:
|
||||||
cell = ' | ' + cell
|
cell = " | " + cell
|
||||||
cell = cell + ' ' * (width - l)
|
cell = cell + " " * (width - l)
|
||||||
print(cell, end='\t')
|
print(cell, end="\t")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
||||||
class Database():
|
class Database:
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
CONFLICT_PATTERN = re.compile('\.sync-conflict-\d{8}-\d{6}-\w{7}')
|
CONFLICT_PATTERN = re.compile("\.sync-conflict-\d{8}-\d{6}-\w{7}")
|
||||||
|
|
||||||
def __init__(self, directory):
|
def __init__(self, directory):
|
||||||
self.version = Database.VERSION
|
self.version = Database.VERSION
|
||||||
|
@ -83,18 +82,25 @@ class Database():
|
||||||
return sum(databaseFile.maxSize() for databaseFile in self.data.values())
|
return sum(databaseFile.maxSize() for databaseFile in self.data.values())
|
||||||
|
|
||||||
def totalChecksumSize(self):
|
def totalChecksumSize(self):
|
||||||
return sum(databaseFile.totalChecksumSize() for databaseFile in self.data.values())
|
return sum(
|
||||||
|
databaseFile.totalChecksumSize() for databaseFile in self.data.values()
|
||||||
|
)
|
||||||
|
|
||||||
def getList(self):
|
def getList(self):
|
||||||
self.prune()
|
self.prune()
|
||||||
|
|
||||||
log.info("Finding conflict files")
|
log.info("Finding conflict files")
|
||||||
widgets = [
|
widgets = [
|
||||||
progressbar.AnimatedMarker(), ' ',
|
progressbar.AnimatedMarker(),
|
||||||
progressbar.BouncingBar(), ' ',
|
" ",
|
||||||
progressbar.DynamicMessage('conflicts'), ' ',
|
progressbar.BouncingBar(),
|
||||||
progressbar.DynamicMessage('files'), ' ',
|
" ",
|
||||||
progressbar.DynamicMessage('dir', width=20, precision=20), ' ',
|
progressbar.DynamicMessage("conflicts"),
|
||||||
|
" ",
|
||||||
|
progressbar.DynamicMessage("files"),
|
||||||
|
" ",
|
||||||
|
progressbar.DynamicMessage("dir", width=20, precision=20),
|
||||||
|
" ",
|
||||||
progressbar.Timer(),
|
progressbar.Timer(),
|
||||||
]
|
]
|
||||||
bar = progressbar.ProgressBar(widgets=widgets).start()
|
bar = progressbar.ProgressBar(widgets=widgets).start()
|
||||||
|
@ -104,7 +110,7 @@ class Database():
|
||||||
f += 1
|
f += 1
|
||||||
if not Database.CONFLICT_PATTERN.search(conflictFilename):
|
if not Database.CONFLICT_PATTERN.search(conflictFilename):
|
||||||
continue
|
continue
|
||||||
filename = Database.CONFLICT_PATTERN.sub('', conflictFilename)
|
filename = Database.CONFLICT_PATTERN.sub("", conflictFilename)
|
||||||
key = (root, filename)
|
key = (root, filename)
|
||||||
if key in self.data:
|
if key in self.data:
|
||||||
dataFile = self.data[key]
|
dataFile = self.data[key]
|
||||||
|
@ -116,11 +122,13 @@ class Database():
|
||||||
dataFile.addConflict(filename)
|
dataFile.addConflict(filename)
|
||||||
dataFile.addConflict(conflictFilename)
|
dataFile.addConflict(conflictFilename)
|
||||||
|
|
||||||
bar.update(conflicts=len(self.data), files=f,
|
bar.update(
|
||||||
dir=root[(len(self.directory)+1):])
|
conflicts=len(self.data), files=f, dir=root[(len(self.directory) + 1) :]
|
||||||
|
)
|
||||||
bar.finish()
|
bar.finish()
|
||||||
log.info(
|
log.info(
|
||||||
f"Found {len(self.data)} conflicts, totalling {self.nbFiles()} conflict files.")
|
f"Found {len(self.data)} conflicts, totalling {self.nbFiles()} conflict files."
|
||||||
|
)
|
||||||
|
|
||||||
def getStats(self):
|
def getStats(self):
|
||||||
log.info("Getting stats from conflict files")
|
log.info("Getting stats from conflict files")
|
||||||
|
@ -132,25 +140,38 @@ class Database():
|
||||||
bar.update(f)
|
bar.update(f)
|
||||||
bar.finish()
|
bar.finish()
|
||||||
log.info(
|
log.info(
|
||||||
f"Total file size: {sizeof_fmt(self.totalSize())}, possible save: {sizeof_fmt(self.totalSize() - self.maxSize())}")
|
f"Total file size: {sizeof_fmt(self.totalSize())}, possible save: {sizeof_fmt(self.totalSize() - self.maxSize())}"
|
||||||
|
)
|
||||||
|
|
||||||
def getChecksums(self):
|
def getChecksums(self):
|
||||||
log.info("Checksumming conflict files")
|
log.info("Checksumming conflict files")
|
||||||
widgets = [
|
widgets = [
|
||||||
progressbar.DataSize(), ' of ', progressbar.DataSize('max_value'),
|
progressbar.DataSize(),
|
||||||
' (', progressbar.AdaptiveTransferSpeed(), ') ',
|
" of ",
|
||||||
progressbar.Bar(), ' ',
|
progressbar.DataSize("max_value"),
|
||||||
progressbar.DynamicMessage('dir', width=20, precision=20), ' ',
|
" (",
|
||||||
progressbar.DynamicMessage('file', width=20, precision=20), ' ',
|
progressbar.AdaptiveTransferSpeed(),
|
||||||
progressbar.Timer(), ' ',
|
") ",
|
||||||
|
progressbar.Bar(),
|
||||||
|
" ",
|
||||||
|
progressbar.DynamicMessage("dir", width=20, precision=20),
|
||||||
|
" ",
|
||||||
|
progressbar.DynamicMessage("file", width=20, precision=20),
|
||||||
|
" ",
|
||||||
|
progressbar.Timer(),
|
||||||
|
" ",
|
||||||
progressbar.AdaptiveETA(),
|
progressbar.AdaptiveETA(),
|
||||||
]
|
]
|
||||||
bar = progressbar.DataTransferBar(
|
bar = progressbar.DataTransferBar(
|
||||||
max_value=self.totalChecksumSize(), widgets=widgets).start()
|
max_value=self.totalChecksumSize(), widgets=widgets
|
||||||
|
).start()
|
||||||
f = 0
|
f = 0
|
||||||
for databaseFile in self.data.values():
|
for databaseFile in self.data.values():
|
||||||
bar.update(f, dir=databaseFile.root[(
|
bar.update(
|
||||||
len(self.directory)+1):], file=databaseFile.filename)
|
f,
|
||||||
|
dir=databaseFile.root[(len(self.directory) + 1) :],
|
||||||
|
file=databaseFile.filename,
|
||||||
|
)
|
||||||
f += databaseFile.totalChecksumSize()
|
f += databaseFile.totalChecksumSize()
|
||||||
try:
|
try:
|
||||||
databaseFile.getChecksums()
|
databaseFile.getChecksums()
|
||||||
|
@ -172,9 +193,9 @@ class Database():
|
||||||
databaseFile.takeAction(execute=execute)
|
databaseFile.takeAction(execute=execute)
|
||||||
|
|
||||||
|
|
||||||
class DatabaseFile():
|
class DatabaseFile:
|
||||||
BLOCK_SIZE = 4096
|
BLOCK_SIZE = 4096
|
||||||
RELEVANT_STATS = ('st_mode', 'st_uid', 'st_gid', 'st_size', 'st_mtime')
|
RELEVANT_STATS = ("st_mode", "st_uid", "st_gid", "st_size", "st_mtime")
|
||||||
|
|
||||||
def __init__(self, root, filename):
|
def __init__(self, root, filename):
|
||||||
self.root = root
|
self.root = root
|
||||||
|
@ -260,7 +281,15 @@ class DatabaseFile():
|
||||||
oldChecksum = self.checksums[f]
|
oldChecksum = self.checksums[f]
|
||||||
|
|
||||||
# If it's been already summed, and we have the same inode and same ctime, don't resum
|
# If it's been already summed, and we have the same inode and same ctime, don't resum
|
||||||
if oldStat is None or not isinstance(oldChecksum, int) or oldStat.st_size != newStat.st_size or oldStat.st_dev != newStat.st_dev or oldStat.st_ino != newStat.st_ino or oldStat.st_ctime != newStat.st_ctime or oldStat.st_dev != newStat.st_dev:
|
if (
|
||||||
|
oldStat is None
|
||||||
|
or not isinstance(oldChecksum, int)
|
||||||
|
or oldStat.st_size != newStat.st_size
|
||||||
|
or oldStat.st_dev != newStat.st_dev
|
||||||
|
or oldStat.st_ino != newStat.st_ino
|
||||||
|
or oldStat.st_ctime != newStat.st_ctime
|
||||||
|
or oldStat.st_dev != newStat.st_dev
|
||||||
|
):
|
||||||
self.checksums[f] = None
|
self.checksums[f] = None
|
||||||
|
|
||||||
self.stats[f] = newStat
|
self.stats[f] = newStat
|
||||||
|
@ -270,7 +299,10 @@ class DatabaseFile():
|
||||||
self.checksums = [False] * len(self.conflicts)
|
self.checksums = [False] * len(self.conflicts)
|
||||||
|
|
||||||
# If all the files are the same inode, set as same files
|
# If all the files are the same inode, set as same files
|
||||||
if len(set([s.st_ino for s in self.stats])) == 1 and len(set([s.st_dev for s in self.stats])) == 1:
|
if (
|
||||||
|
len(set([s.st_ino for s in self.stats])) == 1
|
||||||
|
and len(set([s.st_dev for s in self.stats])) == 1
|
||||||
|
):
|
||||||
self.checksums = [True] * len(self.conflicts)
|
self.checksums = [True] * len(self.conflicts)
|
||||||
|
|
||||||
def getChecksums(self):
|
def getChecksums(self):
|
||||||
|
@ -282,7 +314,7 @@ class DatabaseFile():
|
||||||
if self.checksums[f] is not None:
|
if self.checksums[f] is not None:
|
||||||
continue
|
continue
|
||||||
self.checksums[f] = 1
|
self.checksums[f] = 1
|
||||||
filedescs[f] = open(self.getPath(conflict), 'rb')
|
filedescs[f] = open(self.getPath(conflict), "rb")
|
||||||
|
|
||||||
while len(filedescs):
|
while len(filedescs):
|
||||||
toClose = set()
|
toClose = set()
|
||||||
|
@ -305,12 +337,13 @@ class DatabaseFile():
|
||||||
|
|
||||||
def getFeatures(self):
|
def getFeatures(self):
|
||||||
features = dict()
|
features = dict()
|
||||||
features['name'] = self.conflicts
|
features["name"] = self.conflicts
|
||||||
features['sum'] = self.checksums
|
features["sum"] = self.checksums
|
||||||
for statName in DatabaseFile.RELEVANT_STATS:
|
for statName in DatabaseFile.RELEVANT_STATS:
|
||||||
# Rounding beause I Syncthing also rounds
|
# Rounding beause I Syncthing also rounds
|
||||||
features[statName] = [
|
features[statName] = [
|
||||||
int(stat.__getattribute__(statName)) for stat in self.stats]
|
int(stat.__getattribute__(statName)) for stat in self.stats
|
||||||
|
]
|
||||||
return features
|
return features
|
||||||
|
|
||||||
def getDiffFeatures(self):
|
def getDiffFeatures(self):
|
||||||
|
@ -327,7 +360,7 @@ class DatabaseFile():
|
||||||
if match:
|
if match:
|
||||||
return match[0][15:]
|
return match[0][15:]
|
||||||
else:
|
else:
|
||||||
return '-'
|
return "-"
|
||||||
|
|
||||||
def printInfos(self, diff=True):
|
def printInfos(self, diff=True):
|
||||||
print(os.path.join(self.root, self.filename))
|
print(os.path.join(self.root, self.filename))
|
||||||
|
@ -335,14 +368,13 @@ class DatabaseFile():
|
||||||
features = self.getDiffFeatures()
|
features = self.getDiffFeatures()
|
||||||
else:
|
else:
|
||||||
features = self.getFeatures()
|
features = self.getFeatures()
|
||||||
features['name'] = [DatabaseFile.shortConflict(
|
features["name"] = [DatabaseFile.shortConflict(c) for c in self.conflicts]
|
||||||
c) for c in self.conflicts]
|
table = Table(len(features), len(self.conflicts) + 1)
|
||||||
table = Table(len(features), len(self.conflicts)+1)
|
|
||||||
for x, featureName in enumerate(features.keys()):
|
for x, featureName in enumerate(features.keys()):
|
||||||
table.set(x, 0, featureName)
|
table.set(x, 0, featureName)
|
||||||
for x, featureName in enumerate(features.keys()):
|
for x, featureName in enumerate(features.keys()):
|
||||||
for y in range(len(self.conflicts)):
|
for y in range(len(self.conflicts)):
|
||||||
table.set(x, y+1, features[featureName][y])
|
table.set(x, y + 1, features[featureName][y])
|
||||||
table.print()
|
table.print()
|
||||||
|
|
||||||
def decideAction(self, mostRecent=False):
|
def decideAction(self, mostRecent=False):
|
||||||
|
@ -357,10 +389,10 @@ class DatabaseFile():
|
||||||
if len(features) == 1:
|
if len(features) == 1:
|
||||||
reason = "same files"
|
reason = "same files"
|
||||||
self.action = 0
|
self.action = 0
|
||||||
elif 'st_mtime' in features and mostRecent:
|
elif "st_mtime" in features and mostRecent:
|
||||||
recentTime = features['st_mtime'][0]
|
recentTime = features["st_mtime"][0]
|
||||||
recentIndex = 0
|
recentIndex = 0
|
||||||
for index, time in enumerate(features['st_mtime']):
|
for index, time in enumerate(features["st_mtime"]):
|
||||||
if time > recentTime:
|
if time > recentTime:
|
||||||
recentTime = time
|
recentTime = time
|
||||||
recentIndex = 0
|
recentIndex = 0
|
||||||
|
@ -368,11 +400,11 @@ class DatabaseFile():
|
||||||
reason = "most recent"
|
reason = "most recent"
|
||||||
|
|
||||||
if self.action is None:
|
if self.action is None:
|
||||||
log.warning(
|
log.warning(f"{self.root}/{self.filename}: skip, cause: {reason}")
|
||||||
f"{self.root}/{self.filename}: skip, cause: {reason}")
|
|
||||||
else:
|
else:
|
||||||
log.info(
|
log.info(
|
||||||
f"{self.root}/{self.filename}: keep {DatabaseFile.shortConflict(self.conflicts[self.action])}, cause: {reason}")
|
f"{self.root}/{self.filename}: keep {DatabaseFile.shortConflict(self.conflicts[self.action])}, cause: {reason}"
|
||||||
|
)
|
||||||
|
|
||||||
def takeAction(self, execute=False):
|
def takeAction(self, execute=False):
|
||||||
if self.action is None:
|
if self.action is None:
|
||||||
|
@ -380,7 +412,8 @@ class DatabaseFile():
|
||||||
actionName = self.conflicts[self.action]
|
actionName = self.conflicts[self.action]
|
||||||
if actionName != self.filename:
|
if actionName != self.filename:
|
||||||
log.debug(
|
log.debug(
|
||||||
f"Rename {self.getPath(actionName)} → {self.getPath(self.filename)}")
|
f"Rename {self.getPath(actionName)} → {self.getPath(self.filename)}"
|
||||||
|
)
|
||||||
if execute:
|
if execute:
|
||||||
os.rename(self.getPath(actionName), self.getPath(self.filename))
|
os.rename(self.getPath(actionName), self.getPath(self.filename))
|
||||||
for conflict in self.conflicts:
|
for conflict in self.conflicts:
|
||||||
|
@ -390,22 +423,33 @@ class DatabaseFile():
|
||||||
if execute:
|
if execute:
|
||||||
os.unlink(self.getPath(conflict))
|
os.unlink(self.getPath(conflict))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Handle Syncthing's .sync-conflict files ")
|
description="Handle Syncthing's .sync-conflict files "
|
||||||
|
)
|
||||||
|
|
||||||
# Execution flow
|
# Execution flow
|
||||||
parser.add_argument('directory', metavar='DIRECTORY',
|
parser.add_argument(
|
||||||
nargs='?', help='Directory to analyse')
|
"directory", metavar="DIRECTORY", nargs="?", help="Directory to analyse"
|
||||||
parser.add_argument('-d', '--database',
|
)
|
||||||
help='Database path for file informations')
|
parser.add_argument("-d", "--database", help="Database path for file informations")
|
||||||
parser.add_argument('-r', '--most-recent', action='store_true',
|
parser.add_argument(
|
||||||
help='Always keep the most recent version')
|
"-r",
|
||||||
parser.add_argument('-e', '--execute', action='store_true',
|
"--most-recent",
|
||||||
help='Really apply changes')
|
action="store_true",
|
||||||
parser.add_argument('-p', '--print', action='store_true',
|
help="Always keep the most recent version",
|
||||||
help='Only print differences between files')
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-e", "--execute", action="store_true", help="Really apply changes"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-p",
|
||||||
|
"--print",
|
||||||
|
action="store_true",
|
||||||
|
help="Only print differences between files",
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
@ -419,13 +463,17 @@ if __name__ == "__main__":
|
||||||
if args.database:
|
if args.database:
|
||||||
if os.path.isfile(args.database):
|
if os.path.isfile(args.database):
|
||||||
try:
|
try:
|
||||||
with open(args.database, 'rb') as databaseFile:
|
with open(args.database, "rb") as databaseFile:
|
||||||
database = pickle.load(databaseFile)
|
database = pickle.load(databaseFile)
|
||||||
assert isinstance(database, Database)
|
assert isinstance(database, Database)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
raise ValueError("Not a database file")
|
raise ValueError("Not a database file")
|
||||||
assert database.version <= Database.VERSION, "Version of the loaded database is too recent"
|
assert (
|
||||||
assert database.directory == args.directory, "Directory of the loaded database doesn't match"
|
database.version <= Database.VERSION
|
||||||
|
), "Version of the loaded database is too recent"
|
||||||
|
assert (
|
||||||
|
database.directory == args.directory
|
||||||
|
), "Directory of the loaded database doesn't match"
|
||||||
|
|
||||||
if database is None:
|
if database is None:
|
||||||
database = Database(args.directory)
|
database = Database(args.directory)
|
||||||
|
@ -433,7 +481,7 @@ if __name__ == "__main__":
|
||||||
def saveDatabase():
|
def saveDatabase():
|
||||||
if args.database:
|
if args.database:
|
||||||
global database
|
global database
|
||||||
with open(args.database, 'wb') as databaseFile:
|
with open(args.database, "wb") as databaseFile:
|
||||||
pickle.dump(database, databaseFile)
|
pickle.dump(database, databaseFile)
|
||||||
|
|
||||||
database.getList()
|
database.getList()
|
||||||
|
|
|
@ -25,7 +25,11 @@ if __name__ == "__main__":
|
||||||
)
|
)
|
||||||
parser.add_argument("-p", "--port", env_var="PORT", default=25)
|
parser.add_argument("-p", "--port", env_var="PORT", default=25)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-S", "--security", env_var="SECURITY", choices=["plain", "ssl", "starttls"], default='plain'
|
"-S",
|
||||||
|
"--security",
|
||||||
|
env_var="SECURITY",
|
||||||
|
choices=["plain", "ssl", "starttls"],
|
||||||
|
default="plain",
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument("-l", "--helo", env_var="HELO")
|
parser.add_argument("-l", "--helo", env_var="HELO")
|
||||||
|
@ -67,7 +71,7 @@ if __name__ == "__main__":
|
||||||
args.to = args.receiver
|
args.to = args.receiver
|
||||||
if args.password:
|
if args.password:
|
||||||
password = args.password
|
password = args.password
|
||||||
args.password = '********'
|
args.password = "********"
|
||||||
|
|
||||||
# Transmission content
|
# Transmission content
|
||||||
|
|
||||||
|
@ -163,7 +167,7 @@ Input arguments:
|
||||||
|
|
||||||
# Transmission
|
# Transmission
|
||||||
|
|
||||||
if args.security != 'starttls':
|
if args.security != "starttls":
|
||||||
recv()
|
recv()
|
||||||
send(f"EHLO {args.helo}")
|
send(f"EHLO {args.helo}")
|
||||||
if args.user:
|
if args.user:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import random
|
import random
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
curDir = os.path.realpath('.')
|
curDir = os.path.realpath(".")
|
||||||
assert '.stversions/' in curDir
|
assert ".stversions/" in curDir
|
||||||
tgDir = curDir.replace('.stversions/', '')
|
tgDir = curDir.replace(".stversions/", "")
|
||||||
|
|
||||||
|
|
||||||
for root, dirs, files in os.walk(curDir):
|
for root, dirs, files in os.walk(curDir):
|
||||||
|
@ -17,4 +17,3 @@ for root, dirs, files in os.walk(curDir):
|
||||||
dstPath = os.path.join(dstRoot, dstF)
|
dstPath = os.path.join(dstRoot, dstF)
|
||||||
print(f"{srcPath} → {dstPath}")
|
print(f"{srcPath} → {dstPath}")
|
||||||
shutil.copy2(srcPath, dstPath)
|
shutil.copy2(srcPath, dstPath)
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,6 @@ filenames = sys.argv[2:]
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
assert os.path.isfile(filename)
|
assert os.path.isfile(filename)
|
||||||
exifDict = piexif.load(filename)
|
exifDict = piexif.load(filename)
|
||||||
exifDict['0th'][piexif.ImageIFD.Copyright] = creator.encode()
|
exifDict["0th"][piexif.ImageIFD.Copyright] = creator.encode()
|
||||||
exifBytes = piexif.dump(exifDict)
|
exifBytes = piexif.dump(exifDict)
|
||||||
piexif.insert(exifBytes, filename)
|
piexif.insert(exifBytes, filename)
|
||||||
|
|
||||||
|
|
|
@ -11,22 +11,29 @@ if N < 2:
|
||||||
print("Ben reste chez toi alors.")
|
print("Ben reste chez toi alors.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def trajet_str(a, b):
|
def trajet_str(a, b):
|
||||||
return f"{gares[a]} → {gares[b]}"
|
return f"{gares[a]} → {gares[b]}"
|
||||||
|
|
||||||
|
|
||||||
def chemin_str(stack):
|
def chemin_str(stack):
|
||||||
return ", ".join([trajet_str(stack[i], stack[i+1]) for i in range(len(stack)-1)])
|
return ", ".join(
|
||||||
|
[trajet_str(stack[i], stack[i + 1]) for i in range(len(stack) - 1)]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Demande des prix des trajets
|
# Demande des prix des trajets
|
||||||
|
|
||||||
prices = dict()
|
prices = dict()
|
||||||
|
|
||||||
for i in range(N):
|
for i in range(N):
|
||||||
for j in range(N-1, i, -1):
|
for j in range(N - 1, i, -1):
|
||||||
p = None
|
p = None
|
||||||
while not isinstance(p, float):
|
while not isinstance(p, float):
|
||||||
try:
|
try:
|
||||||
p = float(input(f"Prix du trajet {trajet_str(i, j)} ? ").replace(',', '.'))
|
p = float(
|
||||||
|
input(f"Prix du trajet {trajet_str(i, j)} ? ").replace(",", ".")
|
||||||
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
print("C'est pas un prix ça !")
|
print("C'est pas un prix ça !")
|
||||||
if i not in prices:
|
if i not in prices:
|
||||||
|
@ -40,8 +47,9 @@ miniStack = None
|
||||||
maxiPrice = -inf
|
maxiPrice = -inf
|
||||||
maxiStack = None
|
maxiStack = None
|
||||||
|
|
||||||
|
|
||||||
def register_path(stack):
|
def register_path(stack):
|
||||||
price = sum([prices[stack[i]][stack[i+1]]for i in range(len(stack)-1)])
|
price = sum([prices[stack[i]][stack[i + 1]] for i in range(len(stack) - 1)])
|
||||||
|
|
||||||
global miniPrice, maxiPrice, miniStack, maxiStack
|
global miniPrice, maxiPrice, miniStack, maxiStack
|
||||||
if price < miniPrice:
|
if price < miniPrice:
|
||||||
|
@ -52,6 +60,7 @@ def register_path(stack):
|
||||||
maxiStack = stack.copy()
|
maxiStack = stack.copy()
|
||||||
print(f"{chemin_str(stack)} = {price:.2f} €")
|
print(f"{chemin_str(stack)} = {price:.2f} €")
|
||||||
|
|
||||||
|
|
||||||
stack = [0]
|
stack = [0]
|
||||||
while stack[0] == 0:
|
while stack[0] == 0:
|
||||||
if stack[-1] >= N - 1:
|
if stack[-1] >= N - 1:
|
||||||
|
@ -59,7 +68,7 @@ while stack[0] == 0:
|
||||||
stack.pop()
|
stack.pop()
|
||||||
stack[-1] += 1
|
stack[-1] += 1
|
||||||
else:
|
else:
|
||||||
stack.append(stack[-1]+1)
|
stack.append(stack[-1] + 1)
|
||||||
|
|
||||||
print(f"Prix minimum: {chemin_str(miniStack)} = {miniPrice:.2f} €")
|
print(f"Prix minimum: {chemin_str(miniStack)} = {miniPrice:.2f} €")
|
||||||
print(f"Prix maximum: {chemin_str(maxiStack)} = {maxiPrice:.2f} €")
|
print(f"Prix maximum: {chemin_str(maxiStack)} = {maxiPrice:.2f} €")
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
# pylint: disable=C0103,W0621
|
# pylint: disable=C0103,W0621
|
||||||
|
|
||||||
# pip install tmdbv3api
|
# pip install tmdbv3api
|
||||||
|
@ -20,8 +20,8 @@ Episode = typing.Any # TODO
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
|
|
||||||
API_KEY_PASS_PATH = 'http/themoviedb.org'
|
API_KEY_PASS_PATH = "http/themoviedb.org"
|
||||||
VIDEO_EXTENSIONS = {'mp4', 'mkv', 'avi', 'webm'}
|
VIDEO_EXTENSIONS = {"mp4", "mkv", "avi", "webm"}
|
||||||
|
|
||||||
# Functions
|
# Functions
|
||||||
|
|
||||||
|
@ -31,12 +31,12 @@ def get_pass_data(path: str) -> typing.Dict[str, str]:
|
||||||
Returns the data stored in the Unix password manager
|
Returns the data stored in the Unix password manager
|
||||||
given its path.
|
given its path.
|
||||||
"""
|
"""
|
||||||
run = subprocess.run(['pass', path], stdout=subprocess.PIPE, check=True)
|
run = subprocess.run(["pass", path], stdout=subprocess.PIPE, check=True)
|
||||||
lines = run.stdout.decode().split('\n')
|
lines = run.stdout.decode().split("\n")
|
||||||
data = dict()
|
data = dict()
|
||||||
data['pass'] = lines[0]
|
data["pass"] = lines[0]
|
||||||
for line in lines[1:]:
|
for line in lines[1:]:
|
||||||
match = re.match(r'(\w+): ?(.+)', line)
|
match = re.match(r"(\w+): ?(.+)", line)
|
||||||
if match:
|
if match:
|
||||||
data[match[1]] = match[2]
|
data[match[1]] = match[2]
|
||||||
return data
|
return data
|
||||||
|
@ -44,24 +44,27 @@ def get_pass_data(path: str) -> typing.Dict[str, str]:
|
||||||
|
|
||||||
def confirm(text: str) -> bool:
|
def confirm(text: str) -> bool:
|
||||||
res = input(text + " [yn] ")
|
res = input(text + " [yn] ")
|
||||||
while res not in ('y', 'n'):
|
while res not in ("y", "n"):
|
||||||
res = input("Please answer with y or n: ")
|
res = input("Please answer with y or n: ")
|
||||||
return res == 'y'
|
return res == "y"
|
||||||
|
|
||||||
|
|
||||||
def episode_identifier(episode: typing.Any) -> str:
|
def episode_identifier(episode: typing.Any) -> str:
|
||||||
return f"S{episode['season_number']:02d}E" + \
|
return (
|
||||||
f"{episode['episode_number']:02d} {episode['name']}"
|
f"S{episode['season_number']:02d}E"
|
||||||
|
+ f"{episode['episode_number']:02d} {episode['name']}"
|
||||||
|
)
|
||||||
|
|
||||||
dryrun = '-n' in sys.argv
|
|
||||||
|
dryrun = "-n" in sys.argv
|
||||||
if dryrun:
|
if dryrun:
|
||||||
dryrun = True
|
dryrun = True
|
||||||
sys.argv.remove('-n')
|
sys.argv.remove("-n")
|
||||||
|
|
||||||
|
|
||||||
# Connecting to TMBDB
|
# Connecting to TMBDB
|
||||||
tmdb = tmdbv3api.TMDb()
|
tmdb = tmdbv3api.TMDb()
|
||||||
tmdb.api_key = get_pass_data(API_KEY_PASS_PATH)['api']
|
tmdb.api_key = get_pass_data(API_KEY_PASS_PATH)["api"]
|
||||||
tmdb.language = sys.argv[1]
|
tmdb.language = sys.argv[1]
|
||||||
|
|
||||||
# Searching the TV show name (by current directory name)
|
# Searching the TV show name (by current directory name)
|
||||||
|
@ -71,8 +74,8 @@ if len(sys.argv) >= 3:
|
||||||
show_name = sys.argv[2]
|
show_name = sys.argv[2]
|
||||||
else:
|
else:
|
||||||
show_name = os.path.split(os.path.realpath(os.path.curdir))[1]
|
show_name = os.path.split(os.path.realpath(os.path.curdir))[1]
|
||||||
if '(' in show_name:
|
if "(" in show_name:
|
||||||
show_name = show_name.split('(')[0].strip()
|
show_name = show_name.split("(")[0].strip()
|
||||||
|
|
||||||
search = tv.search(show_name)
|
search = tv.search(show_name)
|
||||||
|
|
||||||
|
@ -86,15 +89,14 @@ for res in search:
|
||||||
break
|
break
|
||||||
|
|
||||||
if not show:
|
if not show:
|
||||||
print("Could not find a matching " +
|
print("Could not find a matching " + f"show on TheMovieDatabase for {show_name}.")
|
||||||
f"show on TheMovieDatabase for {show_name}.")
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Retrieving all the episode of the show
|
# Retrieving all the episode of the show
|
||||||
episodes: typing.List[Episode] = list()
|
episodes: typing.List[Episode] = list()
|
||||||
|
|
||||||
print(f"List of episodes for {show.name}:")
|
print(f"List of episodes for {show.name}:")
|
||||||
for season_number in range(0, show.number_of_seasons+1):
|
for season_number in range(0, show.number_of_seasons + 1):
|
||||||
season_details = season.details(show.id, season_number)
|
season_details = season.details(show.id, season_number)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -119,22 +121,22 @@ for root, dirs, files in os.walk(os.path.curdir):
|
||||||
print(f"- {filename}")
|
print(f"- {filename}")
|
||||||
|
|
||||||
|
|
||||||
def get_episode(season_number: int, episode_number: int
|
def get_episode(season_number: int, episode_number: int) -> typing.Optional[Episode]:
|
||||||
) -> typing.Optional[Episode]:
|
|
||||||
# TODO Make more efficient using indexing
|
# TODO Make more efficient using indexing
|
||||||
for episode in episodes:
|
for episode in episodes:
|
||||||
if episode['season_number'] == season_number \
|
if (
|
||||||
and episode['episode_number'] == episode_number:
|
episode["season_number"] == season_number
|
||||||
|
and episode["episode_number"] == episode_number
|
||||||
|
):
|
||||||
return episode
|
return episode
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# Matching movie files to episode
|
# Matching movie files to episode
|
||||||
associations: typing.List[typing.Tuple[typing.Tuple[str,
|
associations: typing.List[typing.Tuple[typing.Tuple[str, str], Episode]] = list()
|
||||||
str], Episode]] = list()
|
|
||||||
for video in videos:
|
for video in videos:
|
||||||
root, filename = video
|
root, filename = video
|
||||||
match = re.search(r'S(\d+)E(\d+)', filename)
|
match = re.search(r"S(\d+)E(\d+)", filename)
|
||||||
print(f"Treating file: {root}/{filename}")
|
print(f"Treating file: {root}/{filename}")
|
||||||
episode = None
|
episode = None
|
||||||
season_number = 0
|
season_number = 0
|
||||||
|
@ -155,7 +157,8 @@ for video in videos:
|
||||||
episode = get_episode(season_number, episode_number)
|
episode = get_episode(season_number, episode_number)
|
||||||
if not episode:
|
if not episode:
|
||||||
print(
|
print(
|
||||||
f" could not find episode S{season_number:02d}E{episode_number:02d} in TMBD")
|
f" could not find episode S{season_number:02d}E{episode_number:02d} in TMBD"
|
||||||
|
)
|
||||||
|
|
||||||
# Skip
|
# Skip
|
||||||
if not episode:
|
if not episode:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
|
@ -10,7 +10,7 @@ import re
|
||||||
import coloredlogs
|
import coloredlogs
|
||||||
import progressbar
|
import progressbar
|
||||||
|
|
||||||
coloredlogs.install(level='DEBUG', fmt='%(levelname)s %(message)s')
|
coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s")
|
||||||
log = logging.getLogger()
|
log = logging.getLogger()
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
|
@ -18,8 +18,14 @@ SOURCE_FOLDER = os.path.join(os.path.expanduser("~"), "Musiques")
|
||||||
OUTPUT_FOLDER = os.path.join(os.path.expanduser("~"), ".MusiqueCompressed")
|
OUTPUT_FOLDER = os.path.join(os.path.expanduser("~"), ".MusiqueCompressed")
|
||||||
CONVERSIONS = {"flac": "opus"}
|
CONVERSIONS = {"flac": "opus"}
|
||||||
FORBIDDEN_EXTENSIONS = ["jpg", "png", "pdf", "ffs_db"]
|
FORBIDDEN_EXTENSIONS = ["jpg", "png", "pdf", "ffs_db"]
|
||||||
FORGIVEN_FILENAMES = ["cover.jpg", "front.jpg", "folder.jpg",
|
FORGIVEN_FILENAMES = [
|
||||||
"cover.png", "front.png", "folder.png"]
|
"cover.jpg",
|
||||||
|
"front.jpg",
|
||||||
|
"folder.jpg",
|
||||||
|
"cover.png",
|
||||||
|
"front.png",
|
||||||
|
"folder.png",
|
||||||
|
]
|
||||||
IGNORED_EMPTY_FOLDER = [".stfolder"]
|
IGNORED_EMPTY_FOLDER = [".stfolder"]
|
||||||
RESTRICT_CHARACTERS = '[\0\\/:*"<>|]' # FAT32, NTFS
|
RESTRICT_CHARACTERS = '[\0\\/:*"<>|]' # FAT32, NTFS
|
||||||
# RESTRICT_CHARACTERS = '[:/]' # HFS, HFS+
|
# RESTRICT_CHARACTERS = '[:/]' # HFS, HFS+
|
||||||
|
@ -55,7 +61,7 @@ def convertPath(path: str) -> typing.Optional[str]:
|
||||||
extension = extension[1:].lower()
|
extension = extension[1:].lower()
|
||||||
# Remove unwanted characters from filename
|
# Remove unwanted characters from filename
|
||||||
filename_parts = os.path.normpath(filename).split(os.path.sep)
|
filename_parts = os.path.normpath(filename).split(os.path.sep)
|
||||||
filename_parts = [re.sub(RESTRICT_CHARACTERS, '_', part) for part in filename_parts]
|
filename_parts = [re.sub(RESTRICT_CHARACTERS, "_", part) for part in filename_parts]
|
||||||
filename = os.path.sep.join(filename_parts)
|
filename = os.path.sep.join(filename_parts)
|
||||||
# If the extension isn't allowed
|
# If the extension isn't allowed
|
||||||
if extension in FORBIDDEN_EXTENSIONS:
|
if extension in FORBIDDEN_EXTENSIONS:
|
||||||
|
@ -103,7 +109,7 @@ for sourceFile in remainingConversions:
|
||||||
# Converting
|
# Converting
|
||||||
fullSourceFile = os.path.join(SOURCE_FOLDER, sourceFile)
|
fullSourceFile = os.path.join(SOURCE_FOLDER, sourceFile)
|
||||||
if sourceFile == outputFile:
|
if sourceFile == outputFile:
|
||||||
log.debug('%s → %s', fullSourceFile, fullOutputFile)
|
log.debug("%s → %s", fullSourceFile, fullOutputFile)
|
||||||
if act and os.path.isfile(fullOutputFile):
|
if act and os.path.isfile(fullOutputFile):
|
||||||
os.remove(fullOutputFile)
|
os.remove(fullOutputFile)
|
||||||
os.link(fullSourceFile, fullOutputFile)
|
os.link(fullSourceFile, fullOutputFile)
|
||||||
|
@ -113,19 +119,33 @@ for sourceFile in remainingConversions:
|
||||||
log.info("Removing extra files")
|
log.info("Removing extra files")
|
||||||
for extraFile in extraFiles:
|
for extraFile in extraFiles:
|
||||||
fullExtraFile = os.path.join(OUTPUT_FOLDER, extraFile)
|
fullExtraFile = os.path.join(OUTPUT_FOLDER, extraFile)
|
||||||
log.debug('× %s', fullExtraFile)
|
log.debug("× %s", fullExtraFile)
|
||||||
if act:
|
if act:
|
||||||
os.remove(fullExtraFile)
|
os.remove(fullExtraFile)
|
||||||
|
|
||||||
log.info("Listing files that will be converted")
|
log.info("Listing files that will be converted")
|
||||||
for fullSourceFile, fullOutputFile in conversions:
|
for fullSourceFile, fullOutputFile in conversions:
|
||||||
log.debug('%s ⇒ %s', fullSourceFile, fullOutputFile)
|
log.debug("%s ⇒ %s", fullSourceFile, fullOutputFile)
|
||||||
|
|
||||||
log.info("Converting files")
|
log.info("Converting files")
|
||||||
for fullSourceFile, fullOutputFile in progressbar.progressbar(conversions):
|
for fullSourceFile, fullOutputFile in progressbar.progressbar(conversions):
|
||||||
cmd = ["ffmpeg", "-y", "-i", fullSourceFile, "-c:a", "libopus",
|
cmd = [
|
||||||
"-movflags", "+faststart", "-b:a", "128k", "-vbr", "on",
|
"ffmpeg",
|
||||||
"-compression_level", "10", fullOutputFile]
|
"-y",
|
||||||
|
"-i",
|
||||||
|
fullSourceFile,
|
||||||
|
"-c:a",
|
||||||
|
"libopus",
|
||||||
|
"-movflags",
|
||||||
|
"+faststart",
|
||||||
|
"-b:a",
|
||||||
|
"128k",
|
||||||
|
"-vbr",
|
||||||
|
"on",
|
||||||
|
"-compression_level",
|
||||||
|
"10",
|
||||||
|
fullOutputFile,
|
||||||
|
]
|
||||||
if act:
|
if act:
|
||||||
subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -10,11 +10,9 @@ import subprocess
|
||||||
files = sys.argv[1:]
|
files = sys.argv[1:]
|
||||||
|
|
||||||
remove = False
|
remove = False
|
||||||
if '-r' in files:
|
if "-r" in files:
|
||||||
files.remove('-r')
|
files.remove("-r")
|
||||||
remove = True
|
remove = True
|
||||||
|
|
||||||
for f in files:
|
for f in files:
|
||||||
print(os.path.splitext(f))
|
print(os.path.splitext(f))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
import logging
|
import logging
|
||||||
|
|
Loading…
Reference in a new issue