Compare commits
10 Commits
bac386dd98
...
31f02680d9
Author | SHA1 | Date | |
---|---|---|---|
31f02680d9 | |||
3eb3d6054f | |||
4ddfa390ad | |||
420754ed32 | |||
cd30da3426 | |||
b795713018 | |||
588d301f13 | |||
1856d42ab4 | |||
b6cf074362 | |||
bf156cb6e2 |
@ -1,4 +1,5 @@
|
||||
[DEFAULT]
|
||||
camera_type = 0
|
||||
file_extension = 'JPG'
|
||||
trigger_mode = 'key'
|
||||
projects_folder = ''
|
||||
@ -13,8 +14,12 @@ framerate = 16
|
||||
vflip = false
|
||||
hflip = false
|
||||
export_options = 'scale=1920:-1,crop=1920:1080:0:102'
|
||||
cache_images = false
|
||||
[CAMERA]
|
||||
# Nikon D40x
|
||||
# Add meter mode to center, focus mode to fixed selection
|
||||
# /main/capturesettings/autofocusarea to 0
|
||||
# /main/capturesettings/focusmetermode to 1
|
||||
capturemode = 3 # use IR remote
|
||||
imagesize = 2 # use size S (1936x1296)
|
||||
whitebalance = 1 # Natural light
|
||||
|
Binary file not shown.
@ -75,3 +75,5 @@ msgstr ""
|
||||
msgid "Exporting to {}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mp4 files"
|
||||
msgstr "Fichier Mp4"
|
Binary file not shown.
@ -84,3 +84,5 @@ msgstr "Terminaison du processus."
|
||||
msgid "Exporting to {}"
|
||||
msgstr "Exportation dans {}"
|
||||
|
||||
msgid "Mp4 files"
|
||||
msgstr "Fichier Mp4"
|
||||
|
189
main_c.py
189
main_c.py
@ -30,7 +30,7 @@ from io import BytesIO
|
||||
from itertools import count
|
||||
import locale
|
||||
import os
|
||||
from PIL import Image, ImageTk, ImageFilter, ImageDraw, ImageFont
|
||||
from PIL import Image, ImageTk, ImageFilter, ImageDraw, ImageOps, ImageFont
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
@ -60,8 +60,10 @@ from send2trash import send2trash
|
||||
# X Allow opening and exporting without a camera connected
|
||||
# o Better settings names
|
||||
# o webcam support (pygame, win and linux only)
|
||||
# o picam support (picamera2)
|
||||
# X picam support (picamera2)
|
||||
# o notify export ending
|
||||
# o Use try/except for picam and pygame lib import
|
||||
# o picam liveview
|
||||
|
||||
running_from_folder = os.path.realpath(__file__)
|
||||
alphabet = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z']
|
||||
@ -73,6 +75,8 @@ _ = gettext.translation('template', localedir='locales', languages=[LOCALE]).get
|
||||
# Config
|
||||
# defaults
|
||||
project_settings_defaults = {
|
||||
# DSLR = 0, picam = 1, webcam = 2
|
||||
'camera_type': 0,
|
||||
'file_extension':'JPG',
|
||||
'trigger_mode': 'key',
|
||||
'projects_folder': '',
|
||||
@ -87,6 +91,7 @@ project_settings_defaults = {
|
||||
'vflip' : False,
|
||||
'hflip' : False,
|
||||
'export_options' : 'scale=1920:-1,crop=1920:1080:0:102',
|
||||
'cache_images' : False,
|
||||
}
|
||||
|
||||
# Camera Settings (Nikon D40x)
|
||||
@ -102,12 +107,12 @@ for location in config_locations:
|
||||
if os.path.exists( os.path.expanduser(os.path.join(location, 'config.toml'))):
|
||||
with open(os.path.expanduser(location + 'config.toml'), 'rb') as config_file:
|
||||
project_settings = tomllib.load(config_file)
|
||||
if 'DEFAULT' in project_settings:
|
||||
project_settings = project_settings['DEFAULT']
|
||||
if 'CHECK' in project_settings:
|
||||
camera_status = project_settings['CHECK']
|
||||
if 'CAMERA' in project_settings:
|
||||
camera_settings = project_settings['CAMERA']
|
||||
if 'CHECK' in project_settings:
|
||||
camera_settings = project_settings['CHECK']
|
||||
if 'DEFAULT' in project_settings:
|
||||
project_settings = project_settings['DEFAULT']
|
||||
config_found_msg = _("Found configuration file in {}").format(os.path.expanduser(location))
|
||||
print(config_found_msg)
|
||||
|
||||
@ -115,6 +120,8 @@ class KISStopmo(tk.Tk):
|
||||
|
||||
def __init__(self, *args, **kargs):
|
||||
self.check_config()
|
||||
if project_settings['camera_type'] != 0:
|
||||
from picamera2 import Picamera2
|
||||
# Default config
|
||||
# Set script settings according to config file
|
||||
self.onion_skin = project_settings['onion_skin_onstartup']
|
||||
@ -123,9 +130,8 @@ class KISStopmo(tk.Tk):
|
||||
self.fullscreen_bool = project_settings['fullscreen_bool']
|
||||
self.screen_w, self.screen_h = project_settings['screen_w'], project_settings['screen_h']
|
||||
self.framerate = project_settings['framerate']
|
||||
self.playback = False
|
||||
|
||||
# ~ for setting in camera_settings:
|
||||
# ~ print(setting)
|
||||
# ~ self.photo = None
|
||||
self.end_thread = False
|
||||
# Window setup
|
||||
@ -157,51 +163,70 @@ class KISStopmo(tk.Tk):
|
||||
self.img_index = self.check_range(len(self.img_list)-1, False)
|
||||
self.splash_text = _("No images yet! Start shooting...")
|
||||
# Camera setup
|
||||
self.camera = gp.check_result(gp.gp_camera_new())
|
||||
try:
|
||||
gp.check_result(gp.gp_camera_init(self.camera))
|
||||
# get configuration tree
|
||||
self.current_camera_config = gp.check_result(gp.gp_camera_get_config(self.camera))
|
||||
self.apply_camera_settings(self.camera, self.current_camera_config)
|
||||
if self.check_status(self.camera, self.current_camera_config) is False:
|
||||
print(_("Warning: Some settings are not set to the recommended value!"))
|
||||
except:
|
||||
self.splash_text += _("\nCamera not found or busy.")
|
||||
if project_settings['camera_type'] != 0:
|
||||
try:
|
||||
self.camera = Picamera2()
|
||||
self.picam_conf_full = self.camera.create_still_configuration(main={"size":(1920,1080)}, lores={"size":(800,600)})
|
||||
self.camera.configure(self.picam_conf_full)
|
||||
# Autofocus, get lens position and switch to manual mode
|
||||
# Set Af mode to Manual (1). Default is Continuous (2), Auto is 1
|
||||
# TODO: lock exposure, wb
|
||||
self.camera.set_controls({'AfMode':1})
|
||||
self.camera.start(show_preview=False)
|
||||
self.camera.autofocus_cycle()
|
||||
self.camera_lenspos = self.camera.capture_metadata()['LensPosition']
|
||||
self.camera.set_controls({'AfMode':0, 'AwbEnable': False, 'AeEnable': False})
|
||||
self.camera.stop()
|
||||
except:
|
||||
self.camera = False
|
||||
self.splash_text += _("\nCamera not found or busy.")
|
||||
else:
|
||||
self.camera = gp.check_result(gp.gp_camera_new())
|
||||
try:
|
||||
gp.check_result(gp.gp_camera_init(self.camera))
|
||||
# get configuration tree
|
||||
self.current_camera_config = gp.check_result(gp.gp_camera_get_config(self.camera))
|
||||
self.apply_camera_settings(self.camera, self.current_camera_config)
|
||||
if self.check_status(self.camera, self.current_camera_config) is False:
|
||||
print(_("Warning: Some settings are not set to the recommended value!"))
|
||||
except:
|
||||
self.camera = False
|
||||
|
||||
self.splash_text += _("\nCamera not found or busy.")
|
||||
|
||||
self.timeout = 3000 # milliseconds
|
||||
|
||||
self.splashscreen = self.generate_splashscreen()
|
||||
|
||||
image = self.update_image()
|
||||
if image:
|
||||
self.image = None
|
||||
self.update_image()
|
||||
if self.image:
|
||||
if self.onion_skin:
|
||||
if self.img_index:
|
||||
photo = self.apply_onionskin(image, self.onionskin_alpha_default, self.onionskin_fx)
|
||||
photo = self.apply_onionskin(self.image, self.onionskin_alpha_default, self.onionskin_fx)
|
||||
else:
|
||||
photo = ImageTk.PhotoImage(image)
|
||||
|
||||
photo = ImageTk.PhotoImage(self.image)
|
||||
self.label.configure(image=photo)
|
||||
self.label.image = photo
|
||||
|
||||
if project_settings['trigger_mode'] == 'event':
|
||||
root.after(1000, self.trigger_bg_loop)
|
||||
# ~ root.after(1000, self.wait_for_capture)
|
||||
|
||||
# Key binding
|
||||
root.bind("<Escape>", lambda event: root.attributes("-fullscreen", False))
|
||||
root.bind("<f>", lambda event: root.attributes("-fullscreen", True))
|
||||
root.bind("<n>", self.next_frame)
|
||||
root.bind("<b>", self.previous_frame)
|
||||
root.bind("<o>", self.toggle_onionskin)
|
||||
root.bind("<p>", self.preview_animation)
|
||||
root.bind("<e>", self.trigger_export_animation)
|
||||
root.bind("<e>", self.next_frame)
|
||||
root.bind("<j>", self.previous_frame)
|
||||
root.bind("<J>", self.toggle_onionskin)
|
||||
root.bind("<N>", self.preview_animation)
|
||||
root.bind("<E>", self.trigger_export_animation)
|
||||
root.bind("<d>", self.remove_frame)
|
||||
root.bind("<a>", self.print_imglist)
|
||||
|
||||
if project_settings['trigger_mode'] != 'event':
|
||||
root.bind("<j>", self.capture_image)
|
||||
|
||||
|
||||
root.bind("<n>", self.capture_image)
|
||||
|
||||
|
||||
def check_config(self):
|
||||
global project_settings
|
||||
for setting in project_settings_defaults:
|
||||
@ -319,7 +344,6 @@ class KISStopmo(tk.Tk):
|
||||
existing_animation_files = self.img_list
|
||||
# ~ existing_animation_files =
|
||||
file_list = os.listdir(folder)
|
||||
# ~ print(file_list)
|
||||
for file in file_list:
|
||||
if (file.startswith(self.project_letter) and file.endswith(project_settings['file_extension'])):
|
||||
if file not in existing_animation_files:
|
||||
@ -353,7 +377,6 @@ class KISStopmo(tk.Tk):
|
||||
frame_list = self.get_frames_list(folder)
|
||||
counter = (".%04i." % x for x in count(0))
|
||||
# ~ for i in range(len(frame_list)):
|
||||
# ~ print(frame_list)
|
||||
for i in frame_list.keys():
|
||||
# ~ if os.path.exists(os.path.realpath(frame_list[i])):
|
||||
if os.path.exists(os.path.join(folder, i)):
|
||||
@ -385,7 +408,6 @@ class KISStopmo(tk.Tk):
|
||||
if not os.path.exists(frame_path):
|
||||
return 0
|
||||
|
||||
# ~ print(self.img_list)
|
||||
print(_("Removing {}").format(frame_path))
|
||||
# trash file
|
||||
send2trash(frame_path)
|
||||
@ -395,15 +417,10 @@ class KISStopmo(tk.Tk):
|
||||
self.offset_dictvalues(self.img_index)
|
||||
# rename files and get new list
|
||||
self.img_list = self.batch_rename(folder_path)
|
||||
print(self.img_list)
|
||||
self.clean_img_list(folder_path)
|
||||
# ~ print(self.img_list)
|
||||
# update index if possible
|
||||
# ~ print(self.img_index)
|
||||
self.img_index = self.check_range(self.img_index, False)
|
||||
# ~ print(self.img_index)
|
||||
# update display
|
||||
# ~ print(self.img_index)
|
||||
self.update_image(None, self.img_index)
|
||||
|
||||
# ~ def open_jpg(self, filepath:str, w:int, h:int):
|
||||
@ -411,28 +428,22 @@ class KISStopmo(tk.Tk):
|
||||
# If pic not cached
|
||||
if filetuple[-1] is None:
|
||||
try:
|
||||
# ~ image = Image.open(filepath)
|
||||
# ~ print( filetuple[0] + "is not in cache")
|
||||
image = Image.open(os.path.join(self.savepath, filetuple[0]))
|
||||
image = image.resize((w, h))
|
||||
image = ImageOps.fit(image, (w, h))
|
||||
if vflip:
|
||||
image = image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
|
||||
if hflip:
|
||||
image = image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
|
||||
self.img_list[filetuple[0]] = image
|
||||
if project_settings['cache_images']:
|
||||
# TODO : Do not cache image to preserve memory
|
||||
self.img_list[filetuple[0]] = image
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
else:
|
||||
# ~ print( filetuple[0] + "is in cache")
|
||||
image = filetuple[-1]
|
||||
if project_settings['cache_images']:
|
||||
image = filetuple[-1]
|
||||
return image
|
||||
|
||||
|
||||
# ~ def inc(self, x):
|
||||
# ~ if x >= len(self.img_list)-1:
|
||||
# ~ return 0
|
||||
# ~ else:
|
||||
# ~ return x + 1
|
||||
|
||||
def check_range(self, x, loop=True):
|
||||
if x < 0:
|
||||
@ -465,6 +476,7 @@ class KISStopmo(tk.Tk):
|
||||
|
||||
|
||||
def update_image(self, event=None, index=None):
|
||||
# TODO : check event mode still works
|
||||
if event is not None:
|
||||
self.img_index = self.check_range(self.img_index+1)
|
||||
if index is None:
|
||||
@ -484,10 +496,12 @@ class KISStopmo(tk.Tk):
|
||||
if self.onion_skin:
|
||||
if index:
|
||||
photo = self.apply_onionskin(new_image, project_settings['onionskin_alpha_default'], project_settings['onionskin_fx'])
|
||||
# ~ print(photo)
|
||||
self.label.configure(image=photo)
|
||||
self.label.image = photo
|
||||
return new_image
|
||||
# ~ return new_image
|
||||
self.image = new_image
|
||||
# ~ new_image.close()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@ -502,17 +516,31 @@ class KISStopmo(tk.Tk):
|
||||
self.update_image(None, self.img_index)
|
||||
|
||||
|
||||
def playback_animation(self):
|
||||
while self.img_index < len(self.img_list)-1:
|
||||
self.img_index += 1
|
||||
root.after(62, self.update_image(None, self.img_index))
|
||||
root.update_idletasks()
|
||||
|
||||
|
||||
def preview_animation(self, event):
|
||||
# save OS state
|
||||
if self.onion_skin:
|
||||
self.onion_skin = False
|
||||
onion_skin_was_on = True
|
||||
# playback
|
||||
# TODO : Use async function for playback
|
||||
# ~ self.playback = not self.playback
|
||||
# ~ self.img_index = 0
|
||||
# ~ self.playback_animation()
|
||||
for img in self.img_list:
|
||||
# ~ self.update_image(None, self.img_list.index(img))
|
||||
# ~ if self.playback:
|
||||
self.update_image(None, list(self.img_list.keys()).index(img))
|
||||
root.update_idletasks()
|
||||
time.sleep(1/self.framerate)
|
||||
# ~ else:
|
||||
# ~ break
|
||||
# ~ self.update_image(None, self.img_index)
|
||||
self.display_last_frame()
|
||||
# restore OS state
|
||||
@ -632,31 +660,32 @@ class KISStopmo(tk.Tk):
|
||||
print(speed)
|
||||
print(str(current_lightmeter_value) + " - " + str(current_shutterspeed_value))
|
||||
return True
|
||||
# ~ print(str(current_lightmeter_value) + " - " + str(current_shutterspeed_value))
|
||||
|
||||
def capture_image(self, event=None, event_data=None):
|
||||
# get net file name based on prefix, file count and extension
|
||||
next_filename = self.return_next_frame_number(self.get_last_frame(self.savepath))
|
||||
# build full path to file
|
||||
target_path = os.path.join(self.savepath, next_filename)
|
||||
# ~ print(target_path)
|
||||
# Get file from camera
|
||||
if event_data is None:
|
||||
# ~ print("j pressed")
|
||||
new_frame_path = self.camera.capture(gp.GP_CAPTURE_IMAGE)
|
||||
# ~ self.camera.trigger_capture()
|
||||
new_frame = self.camera.file_get(
|
||||
new_frame_path.folder, new_frame_path.name, gp.GP_FILE_TYPE_NORMAL)
|
||||
print(_("Saving {}{}").format(new_frame_path.folder, new_frame_path.name))
|
||||
if project_settings['camera_type'] != 0:
|
||||
self.camera.start(show_preview=False)
|
||||
self.camera.set_controls({"LensPosition": self.camera_lenspos})
|
||||
self.camera.capture_file(target_path, 'main', format='jpeg')
|
||||
self.camera.stop()
|
||||
else:
|
||||
print(_("Getting file {}").format(event_data.name))
|
||||
new_frame = self.camera.file_get(
|
||||
event_data.folder, event_data.name, gp.GP_FILE_TYPE_NORMAL)
|
||||
new_frame.save(target_path)
|
||||
# Get file from DSLR camera
|
||||
if event_data is None:
|
||||
new_frame_path = self.camera.capture(gp.GP_CAPTURE_IMAGE)
|
||||
# ~ self.camera.trigger_capture()
|
||||
new_frame = self.camera.file_get(
|
||||
new_frame_path.folder, new_frame_path.name, gp.GP_FILE_TYPE_NORMAL)
|
||||
print(_("Saving {}{}").format(new_frame_path.folder, new_frame_path.name))
|
||||
else:
|
||||
print(_("Getting file {}").format(event_data.name))
|
||||
new_frame = self.camera.file_get(
|
||||
event_data.folder, event_data.name, gp.GP_FILE_TYPE_NORMAL)
|
||||
new_frame.save(target_path)
|
||||
# ~ next_filename = prefix+next(filename)+ext
|
||||
# ~ print(self.img_list)
|
||||
if '{letter}.-001.JPG'.format(letter=self.project_letter) in self.img_list:
|
||||
# ~ self.img_list.pop('A.-001.JPG')
|
||||
self.img_list.pop('{letter}.-001.JPG'.format(letter=self.project_letter))
|
||||
# Display new frame
|
||||
self.display_last_frame()
|
||||
@ -668,7 +697,6 @@ class KISStopmo(tk.Tk):
|
||||
return
|
||||
event_type, event_data = self.camera.wait_for_event(self.timeout)
|
||||
if event_type == gp.GP_EVENT_FILE_ADDED:
|
||||
# ~ print("file added")
|
||||
self.capture_image(None, event_data)
|
||||
# ~ root.after(self.timeout, self.wait_for_capture)
|
||||
self.wait_for_capture()
|
||||
@ -683,7 +711,8 @@ class KISStopmo(tk.Tk):
|
||||
self.input_filename,
|
||||
self.input_options)
|
||||
.output(
|
||||
self.output_filename,
|
||||
# ~ self.output_filename,
|
||||
self.export_filename,
|
||||
self.output_options
|
||||
)
|
||||
)
|
||||
@ -691,14 +720,22 @@ class KISStopmo(tk.Tk):
|
||||
await ffmpeg.execute()
|
||||
|
||||
def trigger_export_animation(self, event):
|
||||
output_folder = filedialog.askdirectory()
|
||||
self.output_filename = "{folder}{sep}{filename}".format(folder=output_folder, sep=os.sep, filename=self.output_filename)
|
||||
print(_("Exporting to {}").format(self.output_filename))
|
||||
# ~ output_folder = filedialog.askdirectory()
|
||||
self.export_filename = ()
|
||||
self.export_filename = filedialog.asksaveasfilename(defaultextension='.mp4', filetypes=((_("Mp4 files"), '*.mp4'),))
|
||||
if not self.export_filename:
|
||||
# ~ self.export_filename = "{folder}{sep}{filename}".format(folder=os.getcwd(), sep=os.sep, filename=self.output_filename)
|
||||
return False
|
||||
print(_("Exporting to {}").format(self.export_filename))
|
||||
self.export_task = asyncio.run(self.export_animation())
|
||||
# check with self.export_task.done() == True ? https://stackoverflow.com/questions/69350645/proper-way-to-retrieve-the-result-of-tasks-in-asyncio
|
||||
|
||||
def end_bg_loop(KISStopmo):
|
||||
KISStopmo.camera.exit()
|
||||
if KISStopmo.camera is not False:
|
||||
if project_settings['camera_type'] != 0:
|
||||
KISStopmo.camera.stop()
|
||||
else:
|
||||
KISStopmo.camera.exit()
|
||||
KISStopmo.end_thread = True
|
||||
|
||||
root = tk.Tk()
|
||||
|
78
readme.md
Normal file
78
readme.md
Normal file
@ -0,0 +1,78 @@
|
||||
# Stopi
|
||||
|
||||
A python stop-motion script that keeps things simple and allows to use a DSLR or raspicam for capture.
|
||||
|
||||
* Full screen display of the last picture taken
|
||||
* Optional Onion Skinning with the second to last picture
|
||||
* No gui : everything is done with keyboard keys (or a [homemade rpi pico based remote](https://forge.aquilenet.fr/benjamin.duchenne/picoti)) and a config file
|
||||
* Auto configuration of the DSLR on startup
|
||||
* Key or event mode ; use your DSLR's trigger button or IR remote and images are automatically downloaded after capture
|
||||
* Preview playback
|
||||
* Full HD export with ffmpeg
|
||||
* Uses translations
|
||||
|
||||
## Setup
|
||||
|
||||
0. (Windows users only) Setup WSL2 on your (P)OS and install a Debian based distro (Debian, Mint, Ubuntu...)
|
||||
1. Install dependencides : `sudo apt install --no-install-recommends --no-install-suggests git ffmpeg gphoto2 python3-libcamera python3-picamera2 python3-tk`
|
||||
(Optional) If you want a minimal graphical environment : `sudo apt install --no-install-recommends --no-install-suggests openbox xserver-xorg xinit pcmanfm gmrun lxterminal hsetroot unclutter`
|
||||
2. Clone the repo : `git clone https://`
|
||||
3. Change to directory : `cd stopimotion`
|
||||
4. Create Python venv : `python -m venv ./` (If planning to use a raspicam, you need to also pass the `--system-site-packages` parameter to be able to import the GPIO module.)
|
||||
5. Install dependencies : `pip install -r requirements.txt`
|
||||
6. Plug your DSLR/setup your raspicam
|
||||
7. Set Execution bit on script : `chmod +x stopi.sh`
|
||||
8. Launch script : `./stopi.sh`
|
||||
|
||||
## Todo / Fix me
|
||||
|
||||
* UI freezes when exporting
|
||||
* Better settings names ; currently they're kinda cryptic and sucky.
|
||||
* Notify ffmpeg export ending
|
||||
|
||||
## Planned features
|
||||
|
||||
* Liveview (when I get a DSLR that supports it :))
|
||||
* Webcam support (e.g; using pygame)
|
||||
|
||||
## Raspberry Pi image
|
||||
|
||||
For convenience, a disk image is available here for RPI users.
|
||||
|
||||
For advanced users, the steps for preparing RaspiOS for a minimal kiosk-like experience are these :
|
||||
1. Flash Raspi OS bookworm lite version to a SD card, enabling SSH, Wifi, etc if needed.
|
||||
2. Follow the steps in the 'Setup' section above.
|
||||
3. Use the 'raspi-config' utility to enable console auto-login.
|
||||
4. Add this content to '~/.bash_login' :
|
||||
```
|
||||
if [[ -z $DISPLAY ]] && [[ $(tty) = /dev/tty1 ]]; then
|
||||
startx
|
||||
fi
|
||||
```
|
||||
5. Add this content to '~/.xinitrc' :
|
||||
```
|
||||
#!/bin/sh
|
||||
|
||||
# /etc/X11/xinit/xinitrc
|
||||
#
|
||||
# global xinitrc file, used by all X sessions started by xinit (startx)
|
||||
|
||||
# invoke global X session script
|
||||
. /etc/X11/Xsession
|
||||
|
||||
exec openbox-session
|
||||
```
|
||||
6. Add this content to '~/.config/openbox/autostart.sh' :
|
||||
```
|
||||
#!/bin/env bash
|
||||
# Change X keyboard mapping
|
||||
setxkbmap fr
|
||||
# Set background color
|
||||
hsetroot -solid "#8393CC"
|
||||
# Hide mouse after 0.2 seconds
|
||||
unclutter -idle 0.2 &
|
||||
# Start script
|
||||
/home/$USER/stopi.sh &
|
||||
```
|
||||
|
||||
When you reboot, the X session should launch automatically, and then the script.
|
Loading…
Reference in New Issue
Block a user