About Me

Avatar

I should really put something here :-/

Skills

Professional Skills:

  • ITSM Cherwell Administrator
  • Splunk Developer
  • Data Automation & Orchestration
  • Reporting, Alerting, & Visualization
  • Microsoft Power Apps: Power Automate, UI Flow, PowerBi, Teams, Azure DevOps
  • Team Management & Communication

Other Interests:

  • Linux
  • Open-Source
  • FOSS
  • Retro Video Games
  • Travel

PYTHON

gPass

So I wanted a mobile-ish way to manage my pass (password-store) passwords on GNOME for the Librem 5. This works pretty well.

Screenshots

1 2 3

This will go on GitLab at some point soon & be distributed via pip.

Code

#!/usr/bin/env python3.6
# -*- coding: utf-8 -*-
#
#  main.py
#  
#  Copyright 2020 3dom <https://benjamichaelson.keybase.pub>
#  
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#  
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#  
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#  
#  

import os
import gi
import gnupg
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk,GLib

class MainWindow(Gtk.Window):

	def __init__(self):
		Gtk.Window.__init__(self)

		#Set variables

		#Check if password store directory has environment variable & set password store directory
		try:  
			self.pass_store_directory=os.environ['PASSWORD_STORE_DIR']
			print('Environment Variable "PASSWORD_STORE_DIR" is set. Using "%s"'%os.environ['PASSWORD_STORE_DIR'])
		except FileNotFoundError: 
			self.pass_store_directory=os.path.expanduser('~/.password-store/')
			print('Environment Variable "PASSWORD_STORE_DIR" is not set. Using default "~/.password-store/"')

		#Set the working gnupg directory (uses system gnupg directory)
		self.gpg_instance=gnupg.GPG()

		#Set current password-store directory
		self.current_pass_store_directory=self.pass_store_directory

#########################

		#Header bar
		header_bar=Gtk.HeaderBar()
		header_bar.set_show_close_button(True)
		header_bar.props.title='gPass'
		self.set_titlebar(header_bar)

		#"Create Menu" popover
		self.create_menu_popover=Gtk.Popover()

		#"Create Menu" button
		create_menu_button=Gtk.MenuButton('ADD')
		create_menu_button.connect('clicked',self.d_create_menu_clicked)
		create_menu_button.set_popover(self.create_menu_popover)
		header_bar.add(create_menu_button)

		#"Create Menu" button vbox
		self.create_menu_button_vbox=Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
		self.create_menu_popover.add(self.create_menu_button_vbox)

		#"Create Menu" initial button options
		create_password_store_button=Gtk.Button('PASSWORD-STORE')
		create_password_store_button.connect('clicked',self.d_create_password_store_form)
		self.create_menu_button_vbox.pack_end(create_password_store_button,False,False,0)

		#Main layout box
		self.main_box=Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
		self.add(self.main_box)

		#Status bar label
		self.status_bar_label=Gtk.Label()
		self.main_box.add(self.status_bar_label)

		#Scroll window & box
		scroll_window=Gtk.ScrolledWindow()
		scroll_window.set_policy(Gtk.PolicyType.AUTOMATIC,Gtk.PolicyType.AUTOMATIC)
		self.scroll_box=Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
		scroll_window.add_with_viewport(self.scroll_box)
		self.main_box.pack_start(scroll_window,True,True,0)

		#Delete password popover
		self.password_name_delete_menu_popover=Gtk.Popover()

		#Delete password menu button vbox
		self.password_name_delete_button_vbox=Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
		self.password_name_delete_menu_popover.add(self.password_name_delete_button_vbox)

		#Check for pass store
		self.d_check_directories_status()

#########################

	def d_create_menu_clicked(self,button):

		self.create_menu_popover.show_all()

	def d_remove_scrollbox_children(self):

		#Destory any children from passwords box
		for each_child in self.scroll_box.get_children():
			each_child.destroy()

	def d_load_passwords_clicked(self,button,directory):

		#Check that the current directory exists
		if os.path.isdir(directory):

			#Set current pass store directory
			self.current_pass_store_directory=directory

			#Change to current pass-store directory
			os.chdir(self.current_pass_store_directory)

			#Remove scroll box children
			self.d_remove_scrollbox_children()

			#Only walk in current directory
			for (directory_path,directory_names,file_names) in os.walk(self.current_pass_store_directory):

				#Populate passwords box with password-store directories
				for each_directory in sorted(directory_names):
					directory_name_button=Gtk.Button(each_directory)
					directory_name_button.connect('clicked',self.d_load_passwords_clicked,'%s%s/'%(directory_path,each_directory))
					self.scroll_box.pack_start(directory_name_button,False,False,2)

				#Populate passwords box with password-store .gpg files
				for file_name in sorted(file_names):

					#Only load gpg files
					if file_name.endswith('.gpg'):

						#Create password label and buttons
						password_name_box=Gtk.Box()
						password_name_label=Gtk.Label()
						password_name_label.set_markup("<span font_weight='bold'>%s</span>"%file_name)	
						password_name_open_button=Gtk.Button('OPEN')
						password_name_open_button.connect('clicked',self.d_open_password,str.replace('%s/%s'%(self.current_pass_store_directory.split('password-store/',1)[1],os.path.splitext(file_name)[0]),'//','/'))
						password_name_copy_button=Gtk.Button('COPY')
						password_name_copy_button.connect('clicked',self.d_copy_password,str.replace('%s/%s'%(self.current_pass_store_directory.split('password-store/',1)[1],os.path.splitext(file_name)[0]),'//','/'))
						password_name_delete_button_menu=Gtk.MenuButton('DELETE')
						password_name_delete_button_menu.connect('clicked',self.d_password_name_delete_menu_clicked,str.replace('%s/%s'%(self.current_pass_store_directory.split('password-store/',1)[1],os.path.splitext(file_name)[0]),'//','/'))
						password_name_delete_button_menu.set_popover(self.password_name_delete_menu_popover)
						password_name_box.pack_start(password_name_label,False,False,5)
						password_name_box.pack_end(password_name_delete_button_menu,False,False,5)
						password_name_box.pack_end(password_name_open_button,False,False,0)
						password_name_box.pack_end(password_name_copy_button,False,False,5)
						self.scroll_box.pack_start(password_name_box,False,False,2)

				break

			self.scroll_box.show_all()

			#Set current password-store directory as status, removing the directory prefixing password-store root directory
			self.d_set_status('password-store%s'%self.current_pass_store_directory.split('password-store',1)[1])

		#If current directory does not exist, go up a level & load directory
		else:

			if not os.path.isdir(directory):

				#Remove trailing backslash
				directory=directory[:-1]

				while not os.path.isdir(directory):
					directory=directory.rsplit('/',1)[0]
					os.chdir('..')

			self.d_load_passwords_clicked('button','%s/'%os.getcwd())

	#Reset to root password store directory
	def d_set_root_pass_store_directory(self):
		self.current_pass_store_directory=self.pass_store_directory

	#Navigate back. Might be the previous screen, or up a directory
	def d_navigate_back(self,button):

		#Check if in root password-store directory before going back
		if self.current_pass_store_directory == self.pass_store_directory:
			self.d_load_passwords_clicked('button',self.pass_store_directory)

		#Check if not in directory browser. If so, navigating back will enter the last current directory.
		elif self.status_bar_label.get_text()=='NEW PASSWORD' or self.status_bar_label.get_text()=='NEW CATEGORY' or self.status_bar_label.get_text()=='CHOOSE YOUR GPG KEY' or self.status_bar_label.get_text()=='PASSWORD DETAILS':

			self.d_load_passwords_clicked('button',self.current_pass_store_directory)

		else:

			#Enter current directory, go up a level, return current working directory
			os.chdir(self.current_pass_store_directory)
			os.chdir('..')
			self.d_load_passwords_clicked('button','%s/'%os.getcwd())

	def d_clear_screen_clicked(self,button):

		#Remove scroll box children
		self.d_remove_scrollbox_children()

		#Reset current password-store directory
		self.d_set_root_pass_store_directory()

		#Set cleared message as status
		self.d_set_status('CLEARED')

	def d_set_status(self,status_text):
		self.status_bar_label.set_markup("<span foreground='#668cff' size='large' font_weight='bold'>%s</span>"%status_text)

	def d_check_directories_status(self):

		#Check password-store exists
		if os.path.isdir(self.pass_store_directory):

			#If password-store directory found
			print('Password-Store Found In: %s'%self.pass_store_directory)

			#Validate directory exists & set current working directory
			os.chdir(self.pass_store_directory)
			self.pass_store_directory='%s/'%os.getcwd()

			#Reset current password-store directory
			self.d_set_root_pass_store_directory()

			#Add additional "Create Menu" buttons if password-store found
			self.create_password_button=Gtk.Button('PASSWORD')
			self.create_password_button.connect('clicked',self.d_create_password_form)
			self.create_password_category_button=Gtk.Button('CATEGORY')
			self.create_password_category_button.connect('clicked',self.d_create_password_category_form)
			self.create_menu_button_vbox.add(self.create_password_button)
			self.create_menu_button_vbox.add(self.create_password_category_button)

			#Add bottom buttons when password store directory exists

			#Up directory button
			self.navigate_back_button=Gtk.Button("BACK")
			self.navigate_back_button.connect('clicked',self.d_navigate_back)
			self.main_box.pack_start(self.navigate_back_button,False,False,0)

			#"Load Passwords" box
			self.bottom_button_box=Gtk.Box()
			self.main_box.pack_start(self.bottom_button_box,False,False,2)

			load_passwords_button=Gtk.Button('LOAD PASSWORDS')
			load_passwords_button.connect('clicked',self.d_load_passwords_clicked,self.pass_store_directory)
			self.bottom_button_box.pack_start(load_passwords_button,True,True,0)

			clear_screen_button=Gtk.Button('CLEAR SCREEN')
			clear_screen_button.connect('clicked',self.d_clear_screen_clicked)
			self.bottom_button_box.pack_start(clear_screen_button,True,True,0)

			#Show the main box
			self.main_box.show_all()

		else:

			#If password-store directory not found, configure message dialog
			pass_store_missing_message_dialog=Gtk.MessageDialog(self,0,Gtk.MessageType.QUESTION,Gtk.ButtonsType.OK)
			pass_store_missing_message_dialog.set_markup("<span foreground='#e67300' font_weight='heavy'>NO PASSWORD-STORE FOUND</span>")
			pass_store_missing_message_dialog.format_secondary_text('Either the Password-Store is not in the default directory, or you need to create a new store. If you need to use a custom directory, you must configure the Pass environment variable')

			#Run it
			pass_store_missing_message_dialog.run()

			#Destroy it
			pass_store_missing_message_dialog.destroy()

	#Password open screen
	def d_open_password(self,button,password_file):

		#Remove scroll box children
		self.d_remove_scrollbox_children()

		#Open password to screen
		password_open_output=os.popen('pass %s'%password_file).read()

		#Password open name label
		password_open_name_label=Gtk.Label()
		password_open_name_label.set_markup("<span foreground='#e67300' font_weight='heavy'>%s</span>"%password_file)

		#Password open textview
		self.password_open_textview=Gtk.TextView()
		self.password_open_textview.set_property('editable',False)
		self.password_open_textview.set_wrap_mode(1)

		#Set text view buffer
		password_open_textview_buffer=self.password_open_textview.get_buffer()
		password_open_textview_buffer.set_text(password_open_output)

		#Add password text to scroll box
		self.scroll_box.pack_start(password_open_name_label,False,False,5)
		self.scroll_box.pack_start(self.password_open_textview,False,False,5)

		#Set status
		self.d_set_status('PASSWORD DETAILS')

		#Show the scroll box contents
		self.scroll_box.show_all()

	#Copy password to clipboard
	def d_copy_password(self,button,password_file):

		#Copy password to clipboard
		os.popen('pass %s -c'%password_file)

	def d_password_name_delete_menu_clicked(self,button,password_file):

		#Destory any children from password name delete button vbox
		for each_child in self.password_name_delete_button_vbox.get_children():
			each_child.destroy()

		#Delete password button options
		password_name_delete_confirm_button=Gtk.Button('CONFIRM')
		password_name_delete_confirm_button.connect('clicked',self.d_delete_password,password_file)
		password_name_delete_cancel_button=Gtk.Button('CANCEL')
		password_name_delete_cancel_button.connect('clicked',self.d_delete_password,'/null')
		self.password_name_delete_button_vbox.add(password_name_delete_confirm_button)
		self.password_name_delete_button_vbox.add(password_name_delete_cancel_button)

		self.password_name_delete_menu_popover.show_all()

	#Delete password
	def d_delete_password(self,button,password_file):

		#Delete password if confirmed
		if password_file != '/null':

			#Popdown delete password menu
			self.password_name_delete_menu_popover.popdown()

			#Delete password
			os.system('pass rm %s -f'%password_file)

			#Reload current directory
			self.d_load_passwords_clicked('button',self.current_pass_store_directory)

		#Cancel delete password if /null is passed
		else:
			#Popdown delete password menu
			self.password_name_delete_menu_popover.popdown()	

	#Create password form
	def d_create_password_form(self,button):

		#Remove scroll box children & clean screen
		self.d_remove_scrollbox_children()
		self.create_menu_popover.popdown()

		#Password name box
		password_name_box=Gtk.Box()
		password_name_label=Gtk.Label()
		password_name_label.set_markup("<span foreground='#e67300' font_weight='heavy'>NAME</span>")
		self.password_name_entry=Gtk.Entry()
		password_name_box.pack_start(password_name_label,False,False,5)
		password_name_box.pack_end(self.password_name_entry,False,False,5)
		self.scroll_box.pack_start(password_name_box,False,False,5)

		#Create password input box
		password_box=Gtk.Box()
		password_label=Gtk.Label()
		password_label.set_markup("<span foreground='#e67300' font_weight='heavy'>PASSWORD</span>")
		password_length_label=Gtk.Label("Length")
		self.password_length_entry=Gtk.Entry()
		self.password_length_entry.set_text("24")
		self.password_length_entry.set_width_chars(3)
		password_generate_button=Gtk.Button('generate')
		password_generate_button.connect('clicked',self.d_generate_password)
		self.password_entry=Gtk.Entry()
		password_box.pack_start(password_label,False,False,5)
		password_box.pack_end(self.password_entry,False,False,5)
		password_box.pack_end(password_generate_button,False,False,5)
		password_box.pack_end(self.password_length_entry,False,False,5)
		password_box.pack_end(password_length_label,False,False,5)
		self.scroll_box.pack_start(password_box,False,False,5)

		#Create comment input box
		password_comment_box=Gtk.Box()
		password_comment_label=Gtk.Label()
		password_comment_label.set_markup("<span foreground='#e67300' font_weight='heavy'>COMMENT</span>")
		password_comment_textview=Gtk.TextView()
		password_comment_textview.set_wrap_mode(1)

		#Set text view buffer
		self.password_comment_textview_buffer=password_comment_textview.get_buffer()
		self.password_comment_textview_buffer.set_text("username: %semail: %surl: %scomment: %s"%(os.linesep,os.linesep,os.linesep,os.linesep))

		#Add password comment elements to the box
		password_comment_box.pack_start(password_comment_label,False,False,5)
		password_comment_box.pack_end(password_comment_textview,True,True,5)
		self.scroll_box.pack_start(password_comment_box,False,False,5)

		#Create submit button
		password_submit_button=Gtk.Button('SUBMIT')
		password_submit_button.connect('clicked',self.d_submit_create_password)
		self.scroll_box.pack_start(password_submit_button,False,False,5)

		#Set status
		self.d_set_status('NEW PASSWORD')

		#Show the scroll box contents
		self.scroll_box.show_all()

	#Generate password
	def d_generate_password(self,button):

		#Generate password command
		password_generate_output=os.popen("</dev/urandom tr -dc \'A-Za-z0-9!#$%%&\'\\'\'()*+,-./:;<=>?@[\]^_{|}~\' | head -c %s;echo -n"%self.password_length_entry.get_text()).read()

		#Set password entry text
		self.password_entry.set_text(password_generate_output)

	#Create password
	def d_submit_create_password(self,button):

		#Get password text
		password_text=self.password_entry.get_text()

		#Get textview buffer text
		startIter,endIter=self.password_comment_textview_buffer.get_bounds()
		password_comment_textview_buffer_text=self.password_comment_textview_buffer.get_text(startIter,endIter,False)

		#Issue pass new password command for current directory
		os.system('printf "%%s" "%s\n%s" | pass insert %s/%s -m'%(password_text,password_comment_textview_buffer_text,self.current_pass_store_directory.split('password-store',1)[1],self.password_name_entry.get_text()))

		#Load passwords
		self.d_load_passwords_clicked('button',self.current_pass_store_directory)

	#Create password category form
	def d_create_password_category_form(self,button):

		#Remove scroll box children & clean screen
		self.d_remove_scrollbox_children()
		self.create_menu_popover.popdown()

		#Category name field
		category_name_box=Gtk.Box()
		category_name_label=Gtk.Label()
		category_name_label.set_markup("<span foreground='#e67300' font_weight='heavy'>NAME</span>")
		self.category_name_entry=Gtk.Entry()
		category_name_box.pack_start(category_name_label,False,False,5)
		category_name_box.pack_end(self.category_name_entry,False,False,5)
		self.scroll_box.pack_start(category_name_box,False,False,5)

		#Create submit button
		category_submit_button=Gtk.Button('SUBMIT')
		category_submit_button.connect('clicked',self.d_submit_create_category)
		self.scroll_box.pack_start(category_submit_button,False,False,5)

		#Set status
		self.d_set_status('NEW CATEGORY')

		#Show the scroll box contents
		self.scroll_box.show_all()

	#Create password category
	def d_submit_create_category(self,button):
		os.mkdir('%s/%s'%(self.current_pass_store_directory,self.category_name_entry.get_text()))

		#Load passwords
		self.d_load_passwords_clicked('button','%s'%self.current_pass_store_directory)

	#Create password-store option in menu
	def d_create_password_store_form(self,button):

		#Clean screen
		self.create_menu_popover.popdown()
		self.d_remove_scrollbox_children()

		#Set status
		self.d_set_status('CHOOSE YOUR GPG KEY')

		#Only get the uid & keyid from pub keys
		for each_gpg_key in self.gpg_instance.list_keys():

			if each_gpg_key['type'] == 'pub':

				#Create gpg key containers
				gpg_key_box=Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
				gpg_key_inner_box=Gtk.Box()
				gpg_key_uid_label=Gtk.Label()
				gpg_key_uid_label.set_xalign(0)
				gpg_key_box.add(gpg_key_inner_box)
				gpg_key_box.pack_start(gpg_key_uid_label,False,False,0)
				self.scroll_box.pack_start(gpg_key_box,False,False,5)

				#Create inner box widgets
				gpg_key_id_label=Gtk.Label(each_gpg_key['keyid'])
				gpg_key_select_button=Gtk.Button('SELECT')
				gpg_key_select_button.connect('clicked',self.d_submit_create_password_store,each_gpg_key['keyid'])
				gpg_key_inner_box.pack_start(gpg_key_id_label,False,False,5)
				gpg_key_inner_box.pack_end(gpg_key_select_button,False,False,0)

				#Configure markup for key uid label
				escaped_text=GLib.markup_escape_text(each_gpg_key['uids'][0],-1)
				gpg_key_uid_label.set_markup("<span foreground='#e67300' size='x-small' font_weight='heavy'>%s</span>"%escaped_text)

		#Additional info message dialog
		pass_store_creation_message_dialog=Gtk.MessageDialog(self,0,Gtk.MessageType.QUESTION,Gtk.ButtonsType.OK)
		pass_store_creation_message_dialog.set_markup("<span foreground='#e67300' font_weight='heavy'>PASSWORD-STORE CREATION</span>")
		pass_store_creation_message_dialog.format_secondary_text('This will create a new Password-Store using the selected GPG ID. If no GPG keys exist, you need to first create one using GnuPG. Selecting a different key when a Password-Store already exists, will re-encrypt existing files with the new key.')

		#Run it
		pass_store_creation_message_dialog.run()

		#Destroy it
		pass_store_creation_message_dialog.destroy()

		#Show the scroll box contents
		self.scroll_box.show_all()

	#Create password-store
	def d_submit_create_password_store(self,button,gpg_key_id):

		#Check if password-store already exists to update
		if os.path.isdir(self.pass_store_directory):

			#Remove "Create Menu" buttons
			self.create_password_button.destroy()
			self.create_password_category_button.destroy()

			#Remove bottom buttons
			self.navigate_back_button.destroy()
			self.bottom_button_box.destroy()

			#Set "Updated" as status if pass store already existed
			self.d_set_status('UPDATED PASSWORD-STORE')

		else:
			#Set "Created" as status if pass store did not exist
			self.d_set_status('CREATED NEW PASSWORD STORE')

		#Issue pass init command
		os.system('pass init %s'%gpg_key_id)

		#Remove scroll box children
		self.d_remove_scrollbox_children()

		#Check for password store
		self.d_check_directories_status()

win=MainWindow()
win.set_default_size(500,750)
win.connect("destroy", Gtk.main_quit)
win.show_all()
Gtk.main()

LBRY Videos

IMAGE ALT TEXT

https://lbry.tv/@3dom:a

Just a place for ramblings about tech, Linux, Open-Source, FOSS, etc.

HOW TO CONTRIBUTE

JOIN LBRY: INVITE LINK
By joining with this invite code, you are helping this channel. Thank you!

BTC Wallet Address: 32HgMQxNUd3iWRi8TddencUJggqjbtWVjG
Stellar Wallet Address: GD4ZWHLYLM6OCS4PVQJJOPRSAM3B357JOVSJE4TZMVCP5SYWB7WG7EH2
Stellar Federated Address: benjamichaelson*keybase.io

LBRY TIPS:
Just tip in the app! It's greatly appreciated :D

My Keybase Page: https://keybase.io/benjamichaelson Follow Me!

Keybase, KBFS, & Pass Password Store (Unix)

IMAGE ALT TEXT https://lbry.tv/@3dom:a/pass-keybase:c

Summary

On a mission to determined the best combination of security, convenience, ease-of-use, & peace-of-mind for a personal password management strategy, I have settled on Pass & Keybase as my preferred tools. Although this is not intended as a technical tutorial, I will briefly go over each part of my setup and explain how everything fits together.

Pass: https://www.passwordstore.org/

Keybase: https://keybase.io/

HOW TO CONTRIBUTE

JOIN LBRY: INVITE LINK
By joining with this invite code, you are helping contribute to a decentralized video platform, as well as, this channel. Thank you!

BTC Wallet Address: 32HgMQxNUd3iWRi8TddencUJggqjbtWVjG
Stellar Wallet Address: GD4ZWHLYLM6OCS4PVQJJOPRSAM3B357JOVSJE4TZMVCP5SYWB7WG7EH2
Stellar Federated Address: benjamichaelson*keybase.io

LBRY TIPS:
Just tip in the app! It's greatly appreciated :D

My Keybase Page: https://keybase.io/benjamichaelson Follow Me!

OnePlus F-Droid

IMAGE ALT TEXT https://lbry.tv/@3dom:a/video_thumb:5

Summary

Just my experience using the original OnePlus One with the latest LineageOS 17.1, Android 10, F-Droid, & No Google

OnePlus One Specs: https://www.passwordstore.org/

F-Droid: https://keybase.io/

OnePlus One Official LineageOS: https://keybase.io/

LineageOS 17.1 XDA Guide: https://keybase.io/

Librem 5: https://keybase.io/

HOW TO CONTRIBUTE

JOIN LBRY: INVITE LINK
By joining with this invite code, you are helping contribute to a decentralized video platform, as well as, this channel. Thank you!

BTC Wallet Address: 32HgMQxNUd3iWRi8TddencUJggqjbtWVjG
Stellar Wallet Address: GD4ZWHLYLM6OCS4PVQJJOPRSAM3B357JOVSJE4TZMVCP5SYWB7WG7EH2
Stellar Federated Address: benjamichaelson*keybase.io

LBRY TIPS:
Just tip in the app! It's greatly appreciated :D

My Keybase Page: https://keybase.io/benjamichaelson Follow Me!

Avatar

Solus is my distro of choice currently. It does a lot, but not everything. I thought it might be a good idea to document a few things as I come across them.

My hope is that this might benefit you, as well as myself.

Keybase on Solus Linux

There are currently no Keybase App packages for Solus Linux. Follow along to install it manually.

If you trust this code, you may copy it verbatim, to your own .sh script and run.

#!/bin/bash
#Installs Keybase App .DEB on Solus Linux

cd ~/Downloads/
#Go to "Downloads" directory in your home folder

wget https://prerelease.keybase.io/keybase_amd64.deb
#Download "keybase_amd64.deb" file to your "Downloads" directory

mkdir keybase
#Create "keybase" directory for the extracted .deb files

file-roller -e keybase/ keybase_amd64.deb
#Extract .deb files to the newly created "keybase" directory

mkdir keybase/data
#Create "data" directory for extracting "data.tar.xz"

file-roller -e keybase/data/ keybase/data.tar.xz
#Extract data.tar.xz archive to the newly created "data" directory

sudo eopkg install rsync
#Install the "rsync" utility

sudo rsync -avhK keybase/data/ /
#Merge the files in the current "keybase/data" directory to your root system folders, using "rsync" in archive, verbose, human-readable mode, while keeping reciever directory links

sudo /opt/keybase/post_install.sh
#Finally, run the post install "sh" script to complete the Keybase installation.
My Keybase Page: https://keybase.io/benjamichaelson Follow Me!

WireGuard on Solus Linux

There are currently no WireGuard packages for Solus Linux. Follow along to install it manually.

If you trust this code, you may copy it verbatim, to your own .sh script and run.

#!/bin/bash
# Download, build, & install WireGuard on Solus Linux

cd ~/Downloads/
#Navigate to the "Downloads" directory

sudo eopkg install git
#Install Git

git clone https://git.zx2c4.com/wireguard-linux-compat
git clone https://git.zx2c4.com/wireguard-tools
#Clone the WireGuard Git Repository for the module & tool

sudo eopkg install -c system.devel linux-current-headers
#Install the Solus Base Development Tools, current headers, and check installed packages

make -C wireguard-linux-compat/src -j$(nproc)
#Compile WireGuard module source

sudo make -C wireguard-linux-compat/src install
#Install WireGuard module

make -C wireguard-tools/src -j$(nproc)
#Compile WireGuard tool source

sudo make -C wireguard-tools/src install
#Install WireGuard tool

Notes for running WireGuard post install:

This assumes that you already have your keys and configs created & configured.

Secure Config Files:

sudo chown -R root:root /etc/wireguard/
sudo chmod -R 770 /etc/wireguard/

Run at Startup:

sudo systemctl enable wg-quick@wg0.service

Run Config:

wg-quick up wg0- (no .conf)

My Keybase Page: https://keybase.io/benjamichaelson Follow Me!

RetroPie & Emulation Stuff

Progress

Nintendo - Nintendo Entertainment System

NO-INTRO - 2019-12-16

MISSING

Europe Only Titles?
Japan (English) Titles
Bezels
  • Championship Pool (USA)
  • Ferrari Grand Prix Challenge (USA)
  • Jurassic Park (USA)
  • Pinball (Japan, USA)
  • Shooting Range (USA)
  • Vegas Dream (USA)
  • Volleyball (USA, Europe)
  • Wheel of Fortune - Junior Edition (USA)
  • Where's Waldo (USA)
  • World Class Track Meet (USA)

NEC - PC Engine - TurboGrafx 16

NO-INTRO - 2019-12-16

COMPLETE

Nintendo - Game Boy Color

NO-INTRO - 2019-12-16

MISSING

Europe Only Titles?
Japan (English) Titles
Bezels
  • Elevator Action EX (Europe)
  • Le Mans 24 Hours (Europe)
  • Suske en Wiske - De Tijdtemmers ~ Bob et Bobette - Les Dompteurs du Temps (Europe)
  • Three Lions (Europe)
  • UEFA 2000 (Europe)

Sega - Game Gear

NO-INTRO - 2019-12-16

MISSING

Europe Only Titles?
Japan (English) Titles
Bezels
  • Bonkers Wax Up! (USA, Europe)
  • Championship Hockey (Europe)
  • Chase H.Q. (USA)
  • Cheese Cat-Astrophe Starring Speedy Gonzales (USA, Europe)
  • F1 - World Championship Edition (Europe)
  • Jungle Strike (USA)
  • Power Drive (Europe)
  • Scratch Golf (USA)
  • Smurfs Travel the World, The (Europe)
  • Super Golf (USA)
  • Super Kick Off (Europe)

Nintendo - Super Nintendo Entertainment System

NO-INTRO - 2019-12-16

MISSING

Europe Only Titles?
Japan (English) Titles
Bezels
  • Sensible Soccer - European Champions (Europe)

Atari - 2600

NO-INTRO - 2019-12-16 & RomHunter V16.0

MISSING

Europe Only Titles?
Japan (English) Titles
A few roms have missing description metadata due to SselphScraper API errors for ScreenScraper
Bezels
  • 2 Pak Special (Black) - Challenge, Surfing (Europe)
  • 2 Pak Special (Blue) - Dungeon Master, Creature Strike (Europe)
  • Aliens Return (Europe)
  • Maze (USA)
  • Super Baseball (USA)
My Keybase Page: https://keybase.io/benjamichaelson Follow Me!

Compare Files

  • Set a directory that contains the "master" files to compare to
  • Add directories to compare to the list (see code for examples)
  • Generates a ".txt" file for directory containing missing files
  • Optionally, set "delete_missing" to 'yes' to delete files that do not match the "master" files

import os
import glob

#Directory to compare all others against (with no leading '/')
master_dir='/home/user/NAS/RomProject/temp/videos'

#List of directories to compare against the 'master_dir' (with no leading '/')
check_dirs=['/user/crow/NAS/RomProject/temp/roms','/home/user/NAS/RomProject/temp/art']

#Where to save the result .txt files
results_path='/home/user/NAS/RomProject/temp'

#If set to 'yes', deletes files found in 'check_dirs' that are not in the 'master_dir'
delete_missing='no'

#Create blank master list
master_list=[]

#Does the magic...
for each_master_file in os.listdir(master_dir):
	master_list.append(os.path.splitext(each_master_file)[0])

master_list.sort()

for each_check_dir in check_dirs:

	#Create blank directory list to compare to
	check_list=[]

	for each_check_file in os.listdir(each_check_dir):
		check_list.append(os.path.splitext(each_check_file)[0])

	check_list.sort()

	for each_file in master_list:
		if each_file not in check_list:

			#Saves a .txt file with each missing file labeled as the current 'check_dir' name
			with open('%s/%s.txt'%(results_path,os.path.basename(each_check_dir)),'a') as outfile:
				outfile.write('%s\n'%each_file)

	if delete_missing=='yes':
		for each_check_file in check_list:
			if each_check_file not in master_list:
				for current_file in glob.glob('%s/%s.*'%(each_check_dir,each_check_file)):
					os.remove(current_file)
					print('Deleted: %s'%current_file)
My Keybase Page: https://keybase.io/benjamichaelson Follow Me!

Custom Sselph Scraper Script

Create game list file with custom videos & images

  • Since I have my own media, I just needed Sselph Scraper to create the RetroPie "gamelist.xml" and scrape metadata.
  • This will create the "gamelist.xml" for the configured system. You must configure the paths to your preferences, then add the images & video files to those paths. Names of all files must match.
/opt/retropie/supplementary/scraper/scraper -download_images=false -download_images=false \
-console_src=ss -image_dir="/home/pi/.emulationstation/downloaded_images/nes/" \
-image_path="/home/pi/.emulationstation/downloaded_images/nes/" \
-video_dir="/home/pi/.emulationstation/downloaded_videos/nes/" \
-video_path="/home/pi/.emulationstation/downloaded_videos/nes/" -video_suffix="" \
-img_format=png -image_suffix="" -output_file="/home/pi/.emulationstation/gamelists/nes/gamelist.xml" \
-use_filename=true -rom_dir="/home/pi/RetroPie/roms/nes/" -rom_path="/home/pi/RetroPie/roms/nes/"
My Keybase Page: https://keybase.io/benjamichaelson Follow Me!

Zip Roms

Creates One Zip Archive for Each Rom File

Simply fill in the parameters and run the ".sh" script

FILE_EXT=".nes"
IN_FILES="/home/crow/NAS/RomProject/temp/roms/*$FILE_EXT"
OUT_DIR="/home/crow/NAS/RomProject/temp/roms/"

for f in $IN_FILES
do
  base_name=$(basename "$f" $FILE_EXT)
  7za a -tzip "$OUT_DIR$base_name.zip" "$f" -mx9
done
My Keybase Page: https://keybase.io/benjamichaelson Follow Me!

Update Bezel Configs

Updates the bezel ".cfg" files to point to the correct bezel image file

  • Useful if your bezels came preconfigured and the rom names have changed
  • Uses the ".cfg" name to configure the path to the ".png" of the same name

Also creates a new ".cfg" for each rom in "retroarch/config/CORE_NAME/", which points to the overlay ".cfg". Be sure to delete those before running.

Simply fill in the parameters and run the ".sh" script

CONFIG_EXT=".cfg"
BEZEL_EXT=".png"
OVERLAY_CONFIG_DIR="/opt/retropie/configs/all/retroarch/overlay/GameBezels/NES/"
CORE_CONFIG_DIR="/opt/retropie/configs/all/retroarch/config/FCEUmm/"

IN_FILES="$OVERLAY_CONFIG_DIR*$CONFIG_EXT"

for f in $IN_FILES
do
  bezel_name=$(basename "$f" $CONFIG_EXT)
  sed -i -r "s|(overlay0_overlay = \").*(\")|\1$(echo $OVERLAY_CONFIG_DIR$bezel_name$BEZEL_EXT | sed -e 's/\\/\\\\/g; s/\//\\\//g; s/&/\\\&/g')\2|g" "$f"
  echo "input_overlay = \"$f\"" >> "$CORE_CONFIG_DIR$bezel_name$CONFIG_EXT"
  echo "Processing: $bezel_name$CONFIG_EXT"
done
My Keybase Page: https://keybase.io/benjamichaelson Follow Me!

Change File Extensions

Changes all file extensions matching the old extension to the new extension in a directory

Simply fill in the parameters and run the ".sh" script

OLD_FILE_EXT=".PNG"
NEW_FILE_EXT=".png"
DIR="/home/user/NAS/RomProject/temp/art/"

IN_FILES="$DIR*$OLD_FILE_EXT"

for f in $IN_FILES
do
  base_name=$(basename "$f" $OLD_FILE_EXT)
  mv "$f" "$DIR$base_name$NEW_FILE_EXT"

  echo "Processing: $f"

done
My Keybase Page: https://keybase.io/benjamichaelson Follow Me!

Rename Bezel Images

Renames images named "Bezel.png" with parent directories labeled as the rom name

Typically, RocketLauncher has this naming convention

Simply fill in the parameters and run the ".sh" script

DIR="/home/user/NAS/RomProject/temp/bezel-imgs/"

for each_dir in $DIR*/
do
  base_name=$(basename "$each_dir")
  mv "$each_dir"Bezel.png "$DIR$base_name".png

  echo "Processing: $each_dir"

done
My Keybase Page: https://keybase.io/benjamichaelson Follow Me!

Create Bezel Configs

Creates generic config files from a template

Simply fill in the parameters and run the ".sh" script

TEMPLATE_PATH="/home/crow/NAS/RomProject/temp/template.cfg"
CONFIG_EXT=".cfg"
BEZ_EXT=".png"
BEZ_DIR="/home/crow/NAS/RomProject/temp/bezel-imgs/"
OUTPUT_DIR="/home/crow/NAS/RomProject/temp/bezel-configs/"

TEMPLATE_BASE_NAME=$(basename "$TEMPLATE_PATH")
IN_FILES="$BEZ_DIR*$BEZ_EXT"

for f in $IN_FILES
do
  base_name=$(basename "$f" $BEZ_EXT)
  cp "$TEMPLATE_PATH" "$OUTPUT_DIR"
  mv "$OUTPUT_DIR$TEMPLATE_BASE_NAME" "$OUTPUT_DIR$base_name$CONFIG_EXT"

  echo "Processing: $f"

done
My Keybase Page: https://keybase.io/benjamichaelson Follow Me!