110 Commits
v1.5 ... master

Author SHA1 Message Date
365d7ec37b Experimental way to add applications to ratpoison modd during I38 configuration. Removed transfer.sh because I think it's dead. 2025-06-15 02:45:12 -04:00
0368d764c6 Speed optimizations for the menu. 2025-06-07 22:47:42 -04:00
fdc2fdc41d Ocr now copies detected text to the clipboard. 2025-05-31 20:13:34 -04:00
65e6dcf980 Removed ocrdesktop dependency. I38 now handles OCR with a much smaller, simpler and faster program. 2025-05-12 06:07:03 -04:00
09e21e297c Added ability to have applications start in the scratchpad. 2025-04-15 18:31:32 -04:00
7ce6e3d10b Updated the readme. Added power options to the panel mode. It relies on lxsession being installed and is optional, so not necessary for those who prefer to shutdown from the command line. 2025-04-08 19:20:39 -04:00
002c5ce1d7 Totally revamped the alt+f1 menu system. I think this is much better than the old version, and it also fixes a bug where the old menu didn't close when something was selected. It is experimental, and can be removed if people prefer the old menu, just let me know. 2025-04-08 06:02:15 -04:00
70f3cc749e Bluetooth added using blueman if installed. Updated the first run documentation. 2025-04-08 04:36:45 -04:00
100d25773c Experimental magic-wormhole GUI interface added. 2025-04-08 03:56:06 -04:00
ebe5dcf404 Remind moved to the panel, Ctrl+Alt+tab, r. 2025-04-07 15:18:34 -04:00
f41741866a A simple notes system added for the panel, I38 notes. 2025-04-07 15:08:11 -04:00
dc8f832840 Reworked the panel. Now it's a mode, like ratpoison mode. New items include panel mode w for weather, and panel mode s for system information. More coming if this turns out to work well. New dependencies added to readme. 2025-04-06 19:40:55 -04:00
8064eaa6e2 Added vivaldi as browser option. 2025-03-19 14:12:19 -04:00
416fdc7be4 Updated the README. 2025-03-09 23:21:05 -04:00
72721abb22 Toggle screen reader is now a gui, so you can see which one is currently running and switch if you want. 2025-03-09 23:00:32 -04:00
2f2eefddce Created a fake panel. It's technically an 11th workspace, but programs that need to be opened but don't often need interaction are automatically sent there. Use control+alt+tab to access the panel. 2025-03-04 19:59:51 -05:00
5229b3b18a Introduction to I38 help file loads on first start after config generation. 2025-03-03 22:49:46 -05:00
d66d60ee49 Added ability to toggle between Cthulhu and Orca with RP Mode - shift+T. 2024-12-20 06:47:55 -05:00
2990660e61 Added chrome's accessibility flag for the apps that still need it. 2024-12-03 07:50:56 -05:00
ca1b76302d Fixed a typo. 2024-10-23 06:09:21 -04:00
93127aeca3 Add .gitignore for python cache files. 2024-10-23 06:06:29 -04:00
d393677f00 Merge branch 'master' of git.stormux.org:storm/I38 2024-10-23 05:44:07 -04:00
9b8932aca6 Added the grayscale flag to mod+F5 command. This should speed it up quite a bit and make it more accurate for games. 2024-10-23 05:43:49 -04:00
13ba2b6dee Support the Flatpak path.
Now you can launch your Flatpaks from the I38 menu.
2024-09-25 17:32:52 -04:00
732c2b0240 Sway: when reloading the configuration, don't run the sound script always.
I don't think Sway's IPC works the same as it does under I38. It must
be running a different one per each reload, but not sure.

Regardless, you won't get extra sounds on reload now.
2024-09-25 15:57:46 -04:00
0eb4e494ff Adda a note about the custom version of xfce4-notifyd.
I've been using it for weeks now, but didn't think to check the README.
2024-09-23 19:19:40 -04:00
ce22b8347c Fix for reloading and restarting the configuration.
In Sway, you say "command" instead of "run_command" Additionally, take
out the restart since that's not available in Sway.
2024-09-21 16:10:02 -04:00
62c2c06904 Rename script. 2024-09-20 14:00:27 -04:00
12649c0f60 Directory local variables for Emacs contributors.
Sets some handy things for sending patches, might be useful for other contributors.
2024-09-20 14:00:27 -04:00
a63091b320 Put the sensible-terminal script into i38 itself.
There is no such --sensible-terminal flag to Sway, and given it's just
a script we may as well put it in our codebase instead.
2024-09-20 14:00:27 -04:00
faf7ca45e0 When using Sway, include the default distribution files for configuration.
Useful for things such as importing the proper dBus environment.
2024-09-20 14:00:27 -04:00
3769428fdf Added path to ps5 contoller to game controller battery script. 2024-09-17 18:08:36 -04:00
f2ab61b389 Added optional support for x11bell: https://github.com/jovanlanik/x11bell 2024-09-11 02:19:37 -04:00
8c29d40616 Found a sound missing -V0 flag. 2024-08-31 14:26:13 -04:00
31c920de8a OCR through speech-dispatcher added. Interrupt speech-dispatcher key added. 2024-08-13 00:37:39 -04:00
6cf1aac9de Merge branch 'master' of git.stormux.org:storm/I38 2024-07-13 18:55:55 -04:00
3f7f7d7b21 Fixed very chatty error messages on some systems with alsa. 2024-07-13 18:55:32 -04:00
2c763ce6ea Fixed links to licenses. 2024-07-12 19:08:37 -08:00
e68c898700 Fixed a bug in the generation of the customizations file. 2024-06-29 21:52:19 -04:00
607e50294a Updated the README. 2024-06-29 21:19:47 -04:00
05cd390554 The menu categories can now be expanded and collapsed without using shift, so just right arrow to expand and left arrow to collapse. 2024-06-29 20:49:18 -04:00
c0b5c2ee29 Menu categories should now display in alphabetical order. 2024-06-29 19:00:36 -04:00
f5b34aa89c New menu system for I38 that no longer relies on sgtk-menu. 2024-06-29 17:12:13 -04:00
b813753968 New desktop stuff added to 38 script. 2024-06-22 21:01:00 -04:00
0c2b5c3e8b Make sure all files have the #!/usr/bin/env directive. Various bug fixes. Add license header to all files. If you read the logs, make sure to do i38 -u to get the new scripts. Be sure to regenerate the i3 config if you do not have the new bookmark feature which is bound to alt+control+b by default." 2024-06-14 21:53:21 -04:00
c7cf3bff16 Fixed a problem with bookmarks not being written to the correct file. 2024-06-13 00:30:46 -04:00
6eece49afc Fixed problems with bookmark script, added in control+alt+b keybinding. 2024-06-12 23:36:03 -04:00
dd67bdf8a9 Started work on bookmark functionality. 2024-06-12 22:56:44 -04:00
92a9d57c05 Notification sound if using xfce4-notifyd. 2024-05-14 23:35:55 -04:00
ede8e1e11c Prefer xfce4 notofication daemon. It only works with latest orca from git currently, so fall back to notification-daemon if xfce4 notification daemon is not available but notoification-daemon is, otherwise no notifiactions at all. 2024-05-14 16:11:12 -04:00
c8ea6ee9f8 Fixed long standing bug of remind having more than one instance start if I3 was restarted. 2024-04-17 16:12:03 -04:00
82ce8e25d9 Fixed microsoft-edge. 2024-02-10 15:53:10 -05:00
a27fda25f5 Add google-chrome-unstable as an option for browsers. 2024-01-24 12:43:28 -05:00
278ee7a581 Updated the comment at the top of the i3 configuration file. 2023-11-19 16:38:10 -05:00
1b031229a7 Removed lxpolkit because it was causing errors. 2023-10-30 16:55:07 -04:00
e86f2d01f6 Removed lxpolkit because it was causing errors. 2023-10-30 16:53:23 -04:00
07ed90d4fc Forgot to change the prompt to ask about the battery notifications. 2023-10-22 21:00:05 -04:00
65e8c0636a Ask if the battery alert script is wanted during setup if acpi is installed. 2023-10-22 20:54:31 -04:00
2e15449d9a Fixed a bug with restarting remind on i3 restart. Fixed a bug with battery_alert not being loaded. 2023-10-15 11:58:33 -04:00
8e9c2c8a7d Updated the README. 2023-10-13 11:13:17 -04:00
9b539d48ab Battery alert script added for laptops. Provides both speech and sound alerts. 2023-10-11 19:39:26 -04:00
aae01bff20 Make sure any remind process is terminated before restarting i3. This prevents multiples of notifications from happening from multiple instances of the server. 2023-10-04 13:39:35 -04:00
ef882604af Single time remind option added. 2023-10-04 13:21:35 -04:00
eb8fa44757 Hopefully fix it so that notifications aren't limited any more. 2023-10-02 22:31:19 -04:00
6063fac699 Updated keyboard layouts, Thanks Didier for the changes. 2023-10-02 17:04:17 -04:00
6f8b418e80 Fixed a typo in the cancel button for daily reminders. 2023-10-01 14:49:15 -04:00
239d110f58 Create ~/.reminders if remind is detected, this should hopefully prevent the daemon not running error that happens with new users. 2023-10-01 14:45:31 -04:00
b24c0fad91 Try to parse reminders for url, if found and xclip is installed, place the url in the clipboard and notify the reminder without the url. Much nicker for speech. 2023-10-01 02:17:38 -04:00
00a89467fd Set the expire time on notifications so hopefully max notifications exceeded error doesn't happen any more. 2023-09-28 18:00:49 -04:00
9c3cadd7dc Do not bind f to file browser in Ratpoison Mode if no file browser was found. 2023-09-28 09:45:10 -04:00
43bec02a2c Updated reminder dialog with better button icons. 2023-09-28 09:31:01 -04:00
a7746d962c Updated readme with GTK2 configuration as well. 2023-09-28 07:20:22 -06:00
0727ff31c3 Fixed README formatting. 2023-09-28 09:13:15 -04:00
482d8ebed2 Hopefully fixed formatting. 2023-09-28 07:02:54 -06:00
df4182b32f Added another small note about GTK sound themes. 2023-09-28 06:59:58 -06:00
ef030f9cd1 Updated readme, explains how to set gtk sound theme. 2023-09-28 06:58:08 -06:00
e484405684 Updated readme to include libcanberra as an optional dependency. 2023-09-28 06:30:25 -06:00
4d5abd5489 Added login sound option, uses canberra-gtk-play if installed, if not doesn't ask. 2023-09-28 06:27:39 -06:00
cff7288c9e Add daily reminder option added. A second option for viewing today's reminders added. It is view only, and does not contain the delete option like view all reminders has. 2023-09-27 01:35:06 -04:00
b1f413660f Fixed up formatting for hours, minutes, and day of month. Properly handle window close events using cancel or escape. Add monthly reminder option. 2023-09-26 21:49:35 -04:00
1a0ad68ad2 Merge branch 'master' of ssh://git.stormux.org/storm/I38 2023-09-26 06:56:27 -06:00
556b8cb7d8 Added a window close sound to the sound script. 2023-09-26 06:38:02 -06:00
563e0277aa View reminder wil now tell you if there are no reminders. 2023-09-26 08:33:15 -04:00
4e00211c02 Reminder notifications now play a sound before they happen, so should be harder to miss. 2023-09-26 08:26:55 -04:00
e61d0394c3 Add Custom reminder option. 2023-09-25 08:54:51 -04:00
af77a60355 Reminder dialog now handles pressing escape to close. 2023-09-24 14:21:27 -04:00
80cb28dc10 Simple reminder management gui added. For now, only simple recurring reminders can be added. I may try to add more options in the future. This was a bit complex, so watch for bugs and make sure your reminders actually work before depending on its reliability. 2023-09-24 13:40:22 -04:00
2968bbe325 Forgot to remove the background from the remind command. It's handled by the i3 config. 2023-09-22 00:50:52 -04:00
15bf470616 Started work on adding reminders. 2023-09-19 21:45:00 -04:00
19d201ecf4 Experimental multiple keyboard support added. 2023-08-31 01:31:51 -04:00
cb7bfb36e8 Add lxpolkit for policy kit authentication. 2023-08-30 16:08:24 -04:00
9c16367299 Email client bound to alt+e in ratpoison mode. 2023-08-29 16:57:06 -04:00
68620696fc Forgot to actually add some of my changes. 2023-08-29 16:23:51 -04:00
43067c0efe Fixed a couple of problems with screen brightness script. 2023-08-29 16:19:43 -04:00
4773bdae87 Screen brightness script added. 2023-08-29 16:14:11 -04:00
ea7450bb8b Fixed a bug when opening a terminal with RP + c. 2023-07-05 20:43:30 -04:00
50f7bd809a Work on Sway support for the window lister. Unfortunately, most of the window IDs that get returned are "None" so you can't actually select the window, but it does list them now. 2023-06-14 02:05:36 -04:00
1de8cde4cf Updated optional applications list in the README. 2023-06-10 13:21:29 -04:00
7ee783edeb Add ability to have removable storage auto mounted if udiskie is detected. 2023-06-07 00:15:13 -04:00
9c1ac5f7f3 Sound added for custom modes. Default mode sound should only play when switching back to the default mode now. This means we now have ratpoison mode sound, bypassmode sound, default sound for when no mode is being used other than default bindings, and custom, for when you add your on modes in the customizations file. 2023-06-01 15:47:10 -04:00
dca451c996 Workspace switching sound should now work correctly in Sway. 2023-06-01 15:20:52 -04:00
61cdfffc0d Window list should now work for sway. 2023-06-01 15:10:47 -04:00
5851aec8b3 Make the sway option use the sway version of sensible-terminal. 2023-06-01 14:48:52 -04:00
8b30abc8b6 Bypass mode added. In this mode, all keys except the toggle bypass mode key, mod+shift+backspace, will be passed to the application. As far as I know, the toggle key should not conflict with anything. 2023-04-25 19:09:20 -04:00
9efa0b110d this git push brought to you by my inability to type complete words. 2023-04-09 21:48:11 -04:00
c1278a81ee work around for Void Linux weirdness. 2023-04-09 21:22:38 -04:00
fa0c32297c Added --no-startup-id to clipster because it fixes a problem for some users. 2023-04-07 17:13:31 -04:00
c6f97648c3 Found and fixed a bug in non ratpoison mode. It was creating a shit ton of conflicting keybinds. 2023-04-06 16:10:03 -04:00
ec8aa3c00a Modified prompt for using ratpoison mode to show that it is the recommended configuration. 2023-04-06 15:02:23 -04:00
de95c4284a updated the readme file. 2023-04-06 14:55:47 -04:00
32 changed files with 5441 additions and 140 deletions

5
.dir-locals.el Normal file
View File

@ -0,0 +1,5 @@
;;; Directory Local Variables -*- no-byte-compile: t -*-
;;; For more information see (info "(emacs) Directory Variables")
((nil . ((vc-prepare-patches-separately . nil)
(vc-default-patch-addressee . "billy@wolfe.casa"))))

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
**/__pycache__/
*.pyc
*.pyo
*.pyd

325
I38.md Normal file
View File

@ -0,0 +1,325 @@
# Welcome to I38 - Accessible i3 Window Manager
> **Note:** This help guide has been tailored to your specific configuration. You've chosen **BROWSER** as your web browser, **MODKEY** as your mod key, and you're using the **SCREENREADER** screen reader.
## Introduction to I38
I38 is a configuration for the i3 window manager that makes it more accessible for blind people. It features audio feedback, screen reader integration, and keyboard shortcuts designed for non-visual navigation.
Unlike traditional desktop environments like GNOME or MATE, i3 is a tiling window manager, which means windows are arranged in a non-overlapping layout. This can be more efficient to navigate by keyboard, as windows are organized in a predictable structure.
### Coming from GNOME or MATE?
If you're transitioning from GNOME or MATE, here are some key differences to understand:
- **Window Management**: In GNOME/MATE, windows can overlap freely and are typically manipulated with a mouse. In i3/I38, windows tile automatically and are primarily controlled with keyboard shortcuts.
- **Panels and Indicators**: Instead of persistent panels with menus and indicators, I38 uses keyboard shortcuts to access functionality.
- **Workspace Navigation**: While GNOME/MATE have workspaces that you can switch between, I38's workspaces are more central to the workflow and are accessed via dedicated keyboard shortcuts.
- **Application Launching**: Rather than using a start menu or activities overview, I38 provides keyboard shortcuts for launching applications.
I38 has been configured to make this transition easier by providing a tabbed layout (similar to browser tabs) and shortcuts that may feel somewhat familiar.
## Basic Concepts
### Workspaces
Workspaces act like virtual desktops, allowing you to organize applications. You have 10 workspaces available.
- Switch to workspace: `Control` + `F1` through `F10`
- Move window to workspace: `Control` + `Shift` + `F1` through `F10`
*GNOME/MATE comparison:* Similar to workspaces in GNOME/MATE, but with dedicated keyboard shortcuts rather than overview modes or workspace switchers.
### Window Management
Windows in I38 are arranged in a tabbed layout by default, which means windows take up the entire screen and you can switch between them like browser tabs.
- Switch between windows: `Alt` + `Tab` (next) or `Alt` + `Shift` + `Tab` (previous)
- Launch terminal: `MODKEY` + `Return`
- Close window: `MODKEY` + `F4`
- Toggle fullscreen: `MODKEY` + `BackSpace`
- List windows in current workspace: `RATPOISONKEY` then `'` (apostrophe)
*GNOME/MATE comparison:* Alt+Tab works similarly to GNOME/MATE, but window placement is automatic rather than manual.
## Modes in I38
### Default Mode
This is the standard mode for working with applications. Most commands start with your mod key (`MODKEY`).
### Ratpoison Mode
Ratpoison mode allows quick access to common actions using shorter key combinations. To enter Ratpoison mode, press `RATPOISONKEY`. After pressing this key, you can execute commands with single keystrokes.
Common Ratpoison mode commands:
| Key | Action |
|-----|--------|
| `c` | Launch a terminal |
| `e` | Open text editor (TEXTEDITOR) |
| `w` | Launch web browser (BROWSER) |
| `k` | Kill (close) the current window |
| `?` | Show I38 help |
| `Escape` or `Control` + `g` | Exit Ratpoison mode without taking action |
| `Shift` + `c` | Restart Cthulhu screen reader |
| `Shift` + `o` | Restart Orca screen reader |
| `Shift` + `t` | Toggle screen reader |
| `Control` + `;` | Reload I38 configuration |
| `Control` + `q` | Exit i3 (log out) |
| `!` | Open run dialog |
| `Alt` + `b` | Check battery status |
| `g` | Check game controller status |
*GNOME/MATE comparison:* This mode has no direct equivalent in GNOME/MATE. Think of it as a command palette or quick launcher activated by a single key.
### Bypass Mode
Bypass mode passes all keys directly to the application, which is useful for applications that need many keyboard shortcuts. To enter bypass mode, press `MODKEY` + `Shift` + `BackSpace`. Use the same key combination to exit bypass mode.
*GNOME/MATE comparison:* In GNOME/MATE, applications always receive keyboard input directly. Bypass mode simulates this behavior within i3.
## Panel Mode
Panel Mode provides quick access to information displays and utility panels. To enter Panel Mode, press `Alt` + `Control` + `Tab`. A distinctive sound will play when Panel Mode is active.
In Panel Mode, single keypresses launch different information panels:
| Key | Action |
|-----|--------|
| `w` | Display weather information |
| `Shift` + `w` | Open Magic Wormhole file transfer GUI |
| `s` | Show system information |
| `r` | Open reminder panel |
| `n` | Launch notes application |
| `b` | Open bluetooth. *requires blueman be installed at the time of your i3 config generation* |
| `Escape` or `Control` + `g` | Exit Panel Mode without taking action |
Just like Ratpoison Mode, Panel Mode automatically returns you to Default Mode after a selection is made or when you press Escape/Control+g to cancel.
*GNOME/MATE comparison:* Panel Mode replaces the persistent system tray and status indicators used in GNOME/MATE. Instead of having always-visible panels with clickable icons, I38 provides keyboard shortcuts to access this information on demand.
### System Information Panel
The system information panel (`s` key in Panel Mode) displays vital system statistics such as:
- CPU usage
- Memory usage
- Disk space
- Network status
- Battery level (if applicable)
### Weather Panel
The weather panel (`w` key in Panel Mode) provides current weather conditions and forecast information for your configured location.
### File Transfer with Magic Wormhole
The Magic Wormhole panel (`Shift` + `w` in Panel Mode) provides a graphical user interface to the Magic Wormhole command-line application, allowing you to securely share files with others. This offers a more accessible way to use Magic Wormhole's secure file transfer capabilities.
### Notes Application
The notes panel (`n` key in Panel Mode) provides a simple application for creating single-line notes with automatic expiration functionality:
- Create text notes quickly
- Set notes to automatically delete after a specified time period
- Lock important notes to prevent automatic deletion
- Temporary notes will expire after their set time limit
### Reminder Panel
The reminder panel (`r` key in Panel Mode) offers the same reminder functionality described in the Reminders and Notifications section, It was previously in ratpoison mode but has been moved to panel mode because it is a better fit.
*Note:* Because Panel Mode uses a custom implementation rather than a traditional system tray, applications that require a system tray to run may not work with I38.
## Accessibility Features
### Screen Reader
I38 is configured to work with your screen reader (SCREENREADER). The screen reader will provide spoken feedback about what's happening on screen so long as there is a window. If you don't have a window open and need to change something SCREENREADER related, press Control+Alt+d to bring up the desktop, then screen reader keys should work.
- Toggle screen reader: `RATPOISONKEY` then `Shift` + `t`
- Restart screen reader: `RATPOISONKEY` then `Shift` + `o` (for Orca) or `Shift` + `c` (for Cthulhu)
- Interrupt speech: `MODKEY` + `Shift` + `F5`
*GNOME/MATE comparison:* GNOME uses Orca by default with its own keyboard shortcuts. I38 integrates screen readers more deeply with the window manager.
### Braille Display Support
If you've enabled braille display support during setup, I38 will start XBrlAPI automatically to provide braille output from your screen reader.
### OCR (Optical Character Recognition)
If required dependencies are installed, you can use OCR to read text from images or inaccessible applications:
- `MODKEY` + `F5`: Perform OCR on the entire screen and speak the content
- In Ratpoison mode: `Print` or `MODKEY` + `r`: Perform OCR and save to clipboard
*GNOME/MATE comparison:* OCR features are typically not integrated into GNOME/MATE by default.
### Sound Effects
I38 provides audio feedback for many actions:
- Window open/close: Ascending/descending tones
- Mode changes: Distinctive sounds for each mode
- Workspace changes: Subtle audio cues
- Fullscreen toggle: Special sound effect
This audio feedback provides non-visual confirmation of actions and state changes.
*GNOME/MATE comparison:* GNOME/MATE typically have fewer sound effects for window management actions.
## Application Menu and Running Programs
Access applications in multiple ways:
- Applications menu: `MODKEY` + `F1`
- Run dialog (enter a command): `MODKEY` + `F2` or in Ratpoison mode, `!` (exclamation mark)
- Common applications have dedicated shortcuts in Ratpoison mode (see table above)
The applications menu is organized by categories similar to traditional desktop environments.
*GNOME/MATE comparison:* Instead of clicking on application icons or using a start menu, I38 provides keyboard shortcuts to access applications.
## Reminders and Notifications
I38 includes integration with the `remind` program for managing reminders:
- Access the reminder tool: `Alt+Control+Tab` then `r`
- Create various types of reminders (one-time, daily, weekly, monthly)
- Get notification alerts for your reminders
The reminder tool provides the following features:
- **One-time Reminders**: Set for a specific date and time
- **Daily Reminders**: Occur every day at the specified time
- **Weekly Reminders**: Occur on specific days of the week
- **Monthly Reminders**: Occur on a specific day each month or the last day of each month
- **Custom Reminders**: Create complex reminder patterns
*GNOME/MATE comparison:* Similar to calendar applications in GNOME/MATE but with a simplified interface optimized for keyboard navigation.
## Volume and Media Controls
### System Volume
- Increase volume: `MODKEY` + `XF86AudioRaiseVolume`
- Decrease volume: `MODKEY` + `XF86AudioLowerVolume`
- Mute/unmute: `MODKEY` + `XF86AudioMute`
### Media Player Controls
- Play/Pause: `XF86AudioPlay`
- Next track: `XF86AudioNext`
- Previous track: `XF86AudioPrev`
- Stop: `XF86AudioStop`
- Media info: `MODKEY` + `XF86AudioPlay`
In Ratpoison mode, these are also available with Alt+Shift combinations:
- Increase volume: `Alt` + `Shift` + `=`
- Decrease volume: `Alt` + `Shift` + `-`
- Previous track: `Alt` + `Shift` + `z`
- Pause: `Alt` + `Shift` + `c`
- Play: `Alt` + `Shift` + `x`
- Stop: `Alt` + `Shift` + `v`
- Next track: `Alt` + `Shift` + `b`
- Media info: `Alt` + `Shift` + `u`
*GNOME/MATE comparison:* Media controls are similar to those in GNOME/MATE, with the addition of audio feedback.
## File Management
I38 uses FILEBROWSER for file management. Launch it in Ratpoison mode with the `f` key.
*GNOME/MATE comparison:* Similar functionality to Nautilus (GNOME) or Caja (MATE), but launched via keyboard shortcut rather than from a desktop icon or menu.
## System Operations
- Reload I38 configuration: In Ratpoison mode, `Control` + `;` (semicolon)
- Exit i3 (log out): In Ratpoison mode, `Control` + `q`
- Check battery status: In Ratpoison mode, `Alt` + `b`
- Check game controller status: In Ratpoison mode, `g`
- Adjust screen brightness (if xrandr is available): In Ratpoison mode, `Alt` + `s`
*GNOME/MATE comparison:* These functions are typically available through system menus or indicators in GNOME/MATE.
## Keyboard Layouts
Switch between layouts: `Super` + `Space`
This is only available if you chose multiple keyboard layouts during setup.
*GNOME/MATE comparison:* Similar to keyboard layout switching in GNOME/MATE, but with a different default shortcut.
## Desktop and Window Decorations
Unlike GNOME or MATE, i3 uses minimal window decorations. Windows don't have title bars with minimize/maximize buttons. Instead, windows fill their available space automatically, and interactions are performed through keyboard shortcuts.
- Show desktop icons: `MODKEY` + `Control` + `d`
## Clipboard Management
I38 includes clipboard management features:
- Access clipboard history: `MODKEY` + `Control` + `c`
## Bookmark Management
- Access bookmarks: `MODKEY` + `Control` + `b`
*GNOME/MATE comparison:* Bookmarks are typically managed within applications in GNOME/MATE. I38 provides a system-wide bookmark manager.
## Tips for New Users
- **Use the window list**: When you're lost, use `RATPOISONKEY` then `'` to show all windows in the current workspace.
- **Bookmark important websites**: Use `MODKEY` + `Control` + `b` to access bookmarks.
- **Remember the help shortcut**: `MODKEY` + `Shift` + `F1` is your friend when you need guidance.
- **Let the sound effects guide you**: Pay attention to the audio cues to understand what's happening.
- **Take advantage of OCR**: If an application isn't accessible, try the OCR function.
### Transitioning from GNOME/MATE
- Start by learning the basic navigation shortcuts before exploring advanced features
- The tabbed layout should feel somewhat familiar if you're used to browser tabs
- Alt+Tab works similarly to GNOME/MATE for switching between windows
- Focus on keyboard commands rather than looking for visual elements like panels or docks
## Customization
You can customize I38 by editing the file `~/.config/i3/customizations`. This file will not be overwritten when you update I38.
Example customizations:
```
# Change background color
exec_always --no-startup-id xsetroot -solid "#2E3440"
# Add custom keybinding
bindsym $mod+F12 exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ 100%
```
To reconfigure I38 completely, run the `i38.sh` script again.
*GNOME/MATE comparison:* Much more text-based configuration compared to the graphical settings dialogs in GNOME/MATE.
## Getting Help
If you need assistance with I38, you can:
- Press `MODKEY` + `Shift` + `F1` to view the help documentation
- Visit the Stormux website at [stormux.org](https://stormux.org)
- Join the Stormux irc at irc.stormux.org #stormux or #a11y
- Check the i3 documentation at [i3wm.org/docs/userguide.html](https://i3wm.org/docs/userguide.html)
---
*I38 - Making i3 accessible. A Stormux project. License: GPL v3*

688
LICENSE
View File

@ -1,14 +1,674 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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 3 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, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

203
README.md
View File

@ -3,9 +3,10 @@
Accessibility setup script for the i3 window manager.
## i38.sh
Released under the terms of the WTFPL License: http://www.wtfpl.net
Released under the terms of the GPL License Version 3: https://www.gnu.org/licenses/
This is a Stormux project: https://stormux.org
## Why the name?
An uppercase I looks like a 1, 3 from i3, and 8 because the song [We Are 138](https://www.youtube.com/watch?v=-n2Mkdw4q44) rocks!
@ -13,29 +14,213 @@ An uppercase I looks like a 1, 3 from i3, and 8 because the song [We Are 138](ht
## Requirements
- i3-wm: The i3 window manager.
- acpi: [optional] for battery status. It will still work even without this package, but uses it if it is installed.
- dex: [optional] Alternative method for auto starting applications.
- acpi: [optional] for battery status. It will still work even without this package, but uses it if it is installed. Required for the battery monitor with sound alerts.
- bc: For the information panel.
- clipster: clipboard manager
- dex: [optional] Alternative method for auto starting applications.
- i3-wm: The i3 window manager.
- jq: for getting the current workspace
- libcanberra: [optional] To play the desktop login sound.
- libnotify: For sending notifications
- lxsession: [optional] For GUI power options like shutdown
- magic-wormhole: [optional] for file sharing with magic-wormhole GUI
- notification-daemon: To handle notifications
- ocrdesktop: For getting contents of the current window with OCR.
- pamixer: for the mute-unmute script
- pandoc or markdown: To generate html files.
- pcmanfm: [optional] Graphical file manager.
- playerctl: music controls
- python-gobject: for applications menu.
- python-i3ipc: for sounds etc.
- sgtk-menu: for applications menu
- python-pillow: For OCR
- python-pytesseract: For OCR
- remind: [optional] For reminder notifications, Requires notify-daemon and notify-send for automatic reminders.
scrot: For OCR
- sox: for sounds.
- transfersh: [optional] for file sharing GUI
- tesseract: For OCR
- tesseract-data-eng: For OCR
- udiskie: [optional] for automatically mounting removable storage
- x11bell: [optional] Bell support if you do not have a PC speaker. Available from https://github.com/jovanlanik/x11bell
- xbacklight: [optional] for screen brightness adjustment
- xclip: Clipboard support
- xfce4-notifyd: For sending notifications. Replaces notification-daemon (Sway users will need to install the customized variant at <https://github.com/icasdri/xfce4-notifyd-layer-shell>)
- xorg-setxkbmap: [optional] for multiple keyboard layouts
- yad: For screen reader accessible dialogs
I38 will try to detect your browser, file manager, and web browser and present you with a list of options to bind to their launch keys. It will also create bindings for pidgin and mumble if they are installed. To use the bindings, press your ratpoison mode key which is set when you run the i38.sh script. next, press the binding for the application you want, w for web browser, e for text editor, f for file manager, m for mumble, etc. To learn all the bindings, find and read the mode ratpoison section of ~/.config/i3/config.
I38 will try to detect your browser, file manager, and text editor and present you with a list of options to bind to their launch keys. It will also create bindings for pidgin and mumble if they are installed. Additionally, you can now add custom applications with your own keybindings during the setup process. To use the bindings, press your ratpoison mode key which is set when you run the i38.sh script. Next, press the binding for the application you want; w for web browser, e for text editor, f for file manager, m for mumble, etc. To learn all the bindings, find and read the mode ratpoison section of ~/.config/i3/config or use the help binding key, alt or super depending on your settings with Shift+F1.
Ratpoison mode is now enabled by default for better accessibility and ease of use.
Sound themes can be configured using GTK configuration files or gsettings. Replace "name" with the name of the theme you want to use.
Note that if you enable all sound events as shown below, you'll also hear GTK sounds when moving around menus, buttons, etc, if the sound theme has sounds for those events.
To configure the theme name with gsettings, do as follows:
gsettings set org.gnome.desktop.sound theme-name name
If you'd like all sound types enabled:
gsettings set org.gnome.desktop.sound input-feedback-sounds true
gsettings set org.gnome.desktop.sound event-sounds true
To configure with a config file, edit or create ~/.config/gtk-3.0/settings.ini
[Settings]
gtk-enable-event-sounds=1
gtk-enable-input-feedback-sounds=1
gtk-sound-theme-name=name
gtk-modules=canberra-gtk-module
You can apply the same configuration to GTK2 apps. Create or edit ~/.gtkrc-2.0
gtk-enable-event-sounds=1
gtk-enable-input-feedback-sounds=1
gtk-sound-theme-name=name
gtk-modules=canberra-gtk-module
## Usage:
- With no arguments, create the i3 configuration.
- -h: This help screen.
- -h: Help screen.
- -u: Copy over the latest version of scripts.
- -x: Generate ~/.xinitrc and ~/.xprofile.
- -X: Generate ~/.xprofile only.
## Ratpoison Mode
I38 is an adaptation of the old Strychnine project which was based on the Ratpoison window manager. Ratpoison is a screen-like window manager, and the important concept from that, which applies to I38, is adding keyboard shortcuts without conflicting application shortcuts. This is done with an "escape key".
When creating I38, I really wanted to port that functionality over, because it is very powerful and allows for lots and lots of shortcuts while minimizing collisions between shortcuts. So, for example, if you have chosen brave as your web browser, and selected alt+escape as your ratpoison mode key, you can quickly launch brave by pressing alt+escape followed by the letter w.
## Custom Applications in Ratpoison Mode
I38 now includes a powerful system for adding custom applications to ratpoison mode with user-defined keybindings. During the configuration process, after setting up window event sounds, you'll be prompted to add custom applications that will be accessible through ratpoison mode.
### How It Works
The system will prompt you through a series of questions for each application you want to add:
1. **Application Name**: A descriptive name for the application
2. **Execution Command**: The full path or command to run the application
3. **Command Flags**: Optional command-line arguments
4. **Keybinding**: A custom key combination using I38's notation system
### Keybinding Notation System
I38 uses a simple notation system to define keybindings that supports modifiers and special keys:
#### Modifiers
- `^` = Control (Ctrl)
- `!` = Alt
- `#` = Super (Windows/Cmd key)
- `m` = Your chosen mod key (configured during setup)
- Uppercase letters automatically add Shift (e.g., `C` = Shift+c)
#### Special Keys
- **Function Keys**: `f1`, `f2`, ... `f12`
- **Arrow Keys**: `up`, `down`, `left`, `right`
- **Navigation Keys**: `home`, `end`, `pageup`, `pagedown`
- **Editing Keys**: `insert`, `delete`, `backspace`
- **Other Keys**: `space`, `tab`, `return`, `escape`, `print`
#### Example Keybindings
- `c` = Just the 'c' key
- `^c` = Control+c
- `!f1` = Alt+F1
- `mspace` = Your mod key + Space
- `^!up` = Control+Alt+Up arrow
- `#pagedown` = Super+Page Down
- `C` = Shift+c
- `^C` = Control+Shift+c
### Example Configuration Session
Here's what the configuration prompts look like:
```
Enter application name (or press enter when finished): Discord
Enter execution path/command for Discord: discord
Enter command line flags for Discord (optional): --no-sandbox
Enter keybinding for Discord (Examples: c, ^c, !f1, mspace, ^!up) or ? for help: d
```
Another example:
```
Enter application name (or press enter when finished): Terminal Calculator
Enter execution path/command for Terminal Calculator: gnome-calculator
Enter command line flags for Terminal Calculator (optional):
Enter keybinding for Terminal Calculator (Examples: c, ^c, !f1, mspace, ^!up) or ? for help: ^#c
```
### Getting Help During Configuration
If you type `?` when prompted for a keybinding, I38 will display a comprehensive help screen with:
- Complete modifier notation reference
- List of all supported special keys
- Multiple examples of different keybinding combinations
- Rules about uppercase letters and Shift
### Conflict Detection
I38 automatically prevents keybinding conflicts by:
- Maintaining a list of all existing ratpoison mode bindings
- Checking your custom keybinding against reserved keys
- Showing an error message if you try to use a taken keybinding
- Asking you to choose a different key combination
### Reserved Keybindings
The following keys are already used in ratpoison mode and cannot be assigned to custom applications:
- `c` = Terminal
- `e` = Text editor
- `f` = File manager
- `w` = Web browser
- `k` = Kill window
- `m` = Mumble (if installed)
- `p` = Pidgin (if installed)
- `g` = Game controller status
- Various modifier combinations for volume, music controls, etc.
### Using Your Custom Applications
Once configured, access your custom applications by:
1. Press your ratpoison mode key (e.g., Alt+Escape)
2. Press your custom keybinding (e.g., `d` for Discord)
3. The application launches and ratpoison mode exits
### Advanced Examples
**Media Applications:**
- `vlc` bound to `v` for VLC media player
- `spotify` bound to `^s` for Spotify with Control+s
**Development Tools:**
- `code` bound to `mf2` for VS Code with mod+F2
- `gimp` bound to `!g` for GIMP with Alt+g
**System Tools:**
- `htop` bound to `^#h` for system monitor with Control+Super+h
- `pavucontrol` bound to `!space` for audio control with Alt+Space
### Tips for Choosing Keybindings
1. **Keep it memorable**: Use the first letter of the application name when possible
2. **Group related apps**: Use similar modifiers for related applications
3. **Consider frequency**: Use simpler keys for frequently used applications
4. **Avoid conflicts**: The system will warn you, but plan ahead for a logical layout
## Panel Mode
Very similar to ratpoison, accessed with Alt+Control+Tab. It contains items that would normally be found in a traditional desktop's panel. For example, it has a simple note app, system information, weather, bluetooth and power options, and remind.
## I38 Help
To get help for I38, you can press the top level keybinding alt+shift+F1. It is also available by pressing the ratpoison mode key followed by question mark. A limitation of yad, which is used to display the help text means that the cursor starts at the bottom of the text. Please press control+home to jump to the top. You can navigate with the arrow keys, and use control+f to find text within the document.
The help text is a modified version of the configuration file itself that is intended to be easier to read. I have tried to add in comments that should also serve to make things more clear.

644
i38.sh
View File

@ -1,16 +1,26 @@
#!/bin/bash
#!/usr/bin/env bash
# Configures the i3 window manager to make it screen reader accessible
# Written by Storm Dragon, Jeremiah, and contributers.
# Released under the terms of the WTFPL http://www.wtfpl.net
# Copyright Stormux, 2022
# 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 3 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, see <https://www.gnu.org/licenses/>.
# Flag for sway configurations
usingSway=1 # Not by default.
i3Path="${XDG_CONFIG_HOME:-$HOME/.config}/i3"
i3msg="i3-msg"
# Dialog accessibility
export DIALOGOPTS='--no-lines --visit-items'
# Check to make sure minimum requirements are installed.
for i in dialog jq sgtk-menu yad ; do
for i in dialog jq yad ; do
if ! command -v "$i" &> /dev/null ; then
missing+=("$i")
fi
@ -23,6 +33,115 @@ if [[ -n "${missing}" ]]; then
echo "${missing[*]}"
exit 1
fi
if ! command -v pandoc &> /dev/null && ! command -v markdown &> /dev/null ; then
echo "Please install either pandoc or markdown."
echo "The markdown command may be provided by the package discount."
exit 1
fi
keyboard_menu() {
keyboardMenu=("us" "English (US)"
"af" "Dari"
"al" "Albanian"
"et" "Amharic"
"ara" "Arabic"
"ma" "Arabic (Morocco)"
"sy" "Arabic (Syria)"
"am" "Armenian"
"az" "Azerbaijani"
"ml" "Bambara"
"bd" "Bangla"
"by" "Belarusian"
"be" "Belgian"
"dz" "Berber (Algeria, Latin)"
"ba" "Bosnian"
"brai" "Braille"
"bg" "Bulgarian"
"mm" "Burmese"
"cn" "Chinese"
"hr" "Croatian"
"cz" "Czech"
"dk" "Danish"
"mv" "Dhivehi"
"nl" "Dutch"
"bt" "Dzongkha"
"au" "English (Australia)"
"gh" "English (Ghana)"
"ng" "English (Nigeria)"
"za" "English (South Africa)"
"gb" "English (UK)"
"epo" "Esperanto"
"ee" "Estonian"
"fo" "Faroese"
"ph" "Filipino"
"fi" "Finnish"
"fr" "French"
"ca" "French (Canada)"
"cd" "French (Democratic Republic of the Congo)"
"ge" "Georgian"
"de" "German"
"ch" "German (Switzerland)"
"gr" "Greek"
"il" "Hebrew"
"hu" "Hungarian"
"is" "Icelandic"
"in" "Indian"
"jv" "Indonesian (Javanese)"
"iq" "Iraqi"
"ie" "Irish"
"it" "Italian"
"jp" "Japanese"
"nec_vndr/jp" "Japanese (PC-98)"
"kz" "Kazakh"
"kh" "Khmer (Cambodia)"
"kr" "Korean"
"kg" "Kyrgyz"
"la" "Lao"
"lv" "Latvian"
"lt" "Lithuanian"
"mk" "Macedonian"
"mv" "Malay (Jawi, Arabic Keyboard)"
"mt" "Maltese"
"mao" "Maori"
"mn" "Mongolian"
"me" "Montenegrin"
"gn" "N'Ko (AZERTY)"
"np" "Nepali"
"no" "Norwegian"
"ir" "Persian"
"pl" "Polish"
"pt" "Portuguese"
"br" "Portuguese (Brazil)"
"ro" "Romanian"
"ru" "Russian"
"rs" "Serbian"
"lk" "Sinhala (phonetic)"
"sk" "Slovak"
"si" "Slovenian"
"es" "Spanish"
"latam" "Spanish (Latin American)"
"tz" "Swahili (Tanzania)"
"se" "Swedish"
"tw" "Taiwanese"
"tj" "Tajik"
"th" "Thai"
"tr" "Turkish"
"tm" "Turkmen"
"bw" "Tswana"
"ua" "Ukrainian"
"pk" "Urdu (Pakistan)"
"uz" "Uzbek (Afghanistan)"
"vn" "Vietnamese"
"sn" "Wolof"
)
dialog --title "I38" \
--backtitle "Use the arrow keys to find the option you want, and enter to select it. When you are finished selecting layouts, use right arrow to find \"Done\" and press enter." \
--clear \
--cancel-label "Done" \
--no-tags \
--menu "Select Keyboard Layout" 0 0 0 "${keyboardMenu[@]}" --stdout
return $?
}
menulist() {
# Args: List of items for menu.
@ -37,7 +156,7 @@ menulist() {
--backtitle "Use the arrow keys to find the option you want, and enter to select it." \
--clear \
--no-tags \
--menu "$menuText" 0 0 0 ${menuList[@]} --stdout
--menu "$menuText" 0 0 0 "${menuList[@]}" --stdout
return $?
}
@ -57,11 +176,167 @@ yesno() {
# Args: Question to user.
dialog --clear --title "I38" --yesno "$*" -1 -1 --stdout
echo $?
}
# Custom application keybinding functions
declare -A usedKeys
declare -a customApps
showKeybindingHelp() {
dialog --title "I38 Keybinding Help" --msgbox \
"Keybinding Notation:
Modifiers: ^ = Ctrl, ! = Alt, # = Super, m = your mod key
Special keys: f1-f12, up/down/left/right, space, tab, return
home/end/insert/delete/pageup/pagedown, print
backspace, escape
Examples:
c = just 'c' key
^c = Ctrl+c
!f1 = Alt+F1
mspace = mod+Space
^!up = Ctrl+Alt+Up
#pagedown = Super+Page_Down
Uppercase letters imply Shift (e.g., C = Shift+c)" 0 0
}
convertKeybinding() {
local input="$1"
local result=""
local baseKey=""
# Handle modifiers
if [[ "$input" == *"^"* ]]; then
result+="Control+"
input="${input//^/}"
fi
if [[ "$input" == *"!"* ]]; then
result+="Mod1+"
input="${input//!/}"
fi
if [[ "$input" == *"#"* ]]; then
result+="Mod4+"
input="${input//#/}"
fi
if [[ "$input" == *"m"* ]]; then
result+="\$mod+"
input="${input//m/}"
fi
# Handle shift for uppercase letters
if [[ "$input" =~ [A-Z] ]]; then
result+="Shift+"
input="${input,,}"
fi
# Convert special keys
case "$input" in
f[1-9]|f1[0-2]) baseKey="F${input#f}" ;;
up|down|left|right) baseKey="$input" ;;
space) baseKey="space" ;;
tab) baseKey="Tab" ;;
return) baseKey="Return" ;;
escape) baseKey="Escape" ;;
backspace) baseKey="BackSpace" ;;
print) baseKey="Print" ;;
home) baseKey="Home" ;;
end) baseKey="End" ;;
insert) baseKey="Insert" ;;
delete) baseKey="Delete" ;;
pageup) baseKey="Page_Up" ;;
pagedown) baseKey="Page_Down" ;;
*) baseKey="$input" ;;
esac
echo "${result}${baseKey}"
}
populateUsedKeys() {
# Populate with existing ratpoison mode bindings
usedKeys["Shift+slash"]=1
usedKeys["c"]=1
usedKeys["e"]=1
usedKeys["f"]=1
usedKeys["\$mod+e"]=1
usedKeys["w"]=1
usedKeys["k"]=1
usedKeys["m"]=1
usedKeys["Print"]=1
usedKeys["\$mod+r"]=1
usedKeys["p"]=1
usedKeys["\$mod+s"]=1
usedKeys["Mod1+Shift+0"]=1
usedKeys["Mod1+Shift+9"]=1
usedKeys["Mod1+Shift+equal"]=1
usedKeys["Mod1+Shift+minus"]=1
usedKeys["Mod1+Shift+z"]=1
usedKeys["Mod1+Shift+c"]=1
usedKeys["Mod1+Shift+x"]=1
usedKeys["Mod1+Shift+v"]=1
usedKeys["Mod1+Shift+b"]=1
usedKeys["Mod1+Shift+u"]=1
usedKeys["Mod1+b"]=1
usedKeys["g"]=1
usedKeys["apostrophe"]=1
usedKeys["Shift+c"]=1
usedKeys["Shift+o"]=1
usedKeys["Shift+t"]=1
usedKeys["Control+semicolon"]=1
usedKeys["Control+Shift+semicolon"]=1
usedKeys["Shift+exclam"]=1
usedKeys["Control+q"]=1
usedKeys["Escape"]=1
usedKeys["Control+g"]=1
}
inputText() {
# Args: prompt text
dialog --title "I38" --inputbox "$1" 0 0 --stdout
}
addCustomApplication() {
local appName appCommand appFlags keybinding convertedKey
populateUsedKeys
while true; do
appName="$(inputText "Custom Applications:\n\nEnter application name (or press enter when finished):")"
[[ -z "$appName" ]] && break
appCommand="$(inputText "Enter execution path/command for $appName:")"
[[ -z "$appCommand" ]] && continue
appFlags="$(inputText "Enter command line flags for $appName (optional):")"
while true; do
keybinding="$(inputText "Enter keybinding for $appName (Examples: c, ^c, !f1, mspace, ^!up) or ? for help:")"
if [[ "$keybinding" == "?" ]]; then
showKeybindingHelp
continue
fi
[[ -z "$keybinding" ]] && break
convertedKey="$(convertKeybinding "$keybinding")"
if [[ -n "${usedKeys[$convertedKey]}" ]]; then
dialog --title "I38" --msgbox "Keybinding '$keybinding' ($convertedKey) is already in use. Please choose another." 0 0
continue
fi
# Add to arrays
customApps+=("$appName|$appCommand|$appFlags|$convertedKey")
usedKeys["$convertedKey"]=1
break
done
done
}
help() {
echo "${0##*/}"
echo "Released under the terms of the WTFPL License: http://www.wtfpl.net"
echo "Released under the terms of the GPL V3 License: https://www.gnu.org/licenses/"
echo -e "This is a Stormux project: https://stormux.org\n"
echo -e "Usage:\n"
echo "With no arguments, create the i3 configuration."
@ -114,6 +389,7 @@ fi
cat << 'EOF' > ~/.xprofile
# Accessibility variables
export ACCESSIBILITY_ENABLED=1
export CHROME_FLAGS="--force-renderer-accessibility"
export GTK_MODULES=gail:atk-bridge
export GNOME_ACCESSIBILITY=1
export QT_ACCESSIBILITY=1
@ -145,6 +421,8 @@ while getopts "${args}" i ; do
case "$i" in
h) help;;
s)
swaySystemIncludesPath="/etc/sway/config.d"
usingSway=0
i3msg="swaymsg"
i3Path="${XDG_CONFIG_HOME:-$HOME/.config}/sway"
;;
@ -154,15 +432,18 @@ while getopts "${args}" i ; do
esac
done
# Mod1 alt
# Mod4 super
# Mod2 and Mod3 not usually defined.
# Configuration questions
export i3Mode=$(yesno "Would you like to use ratpoison mode? This behaves more like strychnine, with an escape key followed by keybindings.")
# Ratpoison mode is enabled by default
export i3Mode=0
# Prevent setting ratpoison mode key to the same as default mode key
while [[ "$escapeKey" == "$mod" ]]; do
if [[ $i3Mode -eq 0 ]]; then
escapeKey="$(menulist "Ratpoison mode key:" Control+t Control+z Control+Escape Alt+Escape Control+Space Super)"
escapeKey="${escapeKey//Alt/Mod1}"
escapeKey="${escapeKey//Super/Mod4}"
fi
escapeKey="$(menulist "Ratpoison mode key:" Control+t Control+z Control+Escape Alt+Escape Control+Space Super)"
escapeKey="${escapeKey//Alt/Mod1}"
escapeKey="${escapeKey//Super/Mod4}"
mod="$(menulist "I3 mod key, for top level bindings:" Alt Control Super)"
mod="${mod//Alt/Mod1}"
mod="${mod//Super/Mod4}"
@ -170,11 +451,52 @@ while [[ "$escapeKey" == "$mod" ]]; do
dialog --title "I38" --msgbox "Ratpoison and mod key cannot be the same key." -1 -1
fi
done
# Multiple keyboard layouts
if [[ $(yesno "Do you want to use multiple keyboard layouts?") -eq 0 ]]; then
unset kbd
while : ; do
kbd+=("$(keyboard_menu)") || break
done
fi
# Volume jump
volumeJump=$(rangebox "How much should pressing the volume keys change the volume?" 1 15 5)
# Screen Reader
unset programList
for i in cthulhu orca ; do
if command -v ${i/#-/} &> /dev/null ; then
if [ -n "$programList" ]; then
programList="$programList $i"
else
programList="$i"
fi
fi
done
if [ "$programList" != "${programList// /}" ]; then
screenReader="$(menulist ":Screen Reader" $programList)"
else
screenReader="${programList/#-/}"
fi
export screenReader="$(command -v $screenReader)"
# Email client
unset programList
for i in betterbird evolution thunderbird ; do
if command -v ${i/#-/} &> /dev/null ; then
if [ -n "$programList" ]; then
programList="$programList $i"
else
programList="$i"
fi
fi
done
if [ "$programList" != "${programList// /}" ]; then
emailClient="$(menulist "Email client:" $programList)"
else
emailClient="${programList/#-/}"
fi
export emailClient="$(command -v $emailClient)"
# Web browser
unset programList
for i in brave chromium epiphany firefox google-chrome-stable midori seamonkey ; do
for i in brave chromium epiphany firefox google-chrome-stable google-chrome-unstable microsoft-edge-stable microsoft-edge-beta microsoft-edge-dev midori seamonkey vivaldi ; do
if command -v ${i/#-/} &> /dev/null ; then
if [ -n "$programList" ]; then
programList="$programList $i"
@ -188,7 +510,7 @@ if [ "$programList" != "${programList// /}" ]; then
else
webBrowser="${programList/#-/}"
fi
webBrowser="$(command -v $webBrowser)"
export webBrowser="$(command -v $webBrowser)"
# Text editor
unset programList
for i in emacs geany gedit kate kwrite l3afpad leafpad libreoffice mousepad pluma ; do
@ -205,7 +527,7 @@ textEditor="$(menulist "Text editor:" $programList)"
else
textEditor="${programList/#-/}"
fi
textEditor="$(command -v $textEditor)"
export textEditor="$(command -v $textEditor)"
# File browser
# Configure file browser
unset programList
@ -223,19 +545,30 @@ if [ "$programList" != "${programList// /}" ]; then
else
fileBrowser="${programList/#-/}"
fi
fileBrowser="$(command -v $fileBrowser)"
export fileBrowser="$(command -v $fileBrowser)"
# Auto mount removable media
udiskie=1
if command -v udiskie &> /dev/null ; then
export udiskie=$(yesno "Would you like removable drives to automatically mount when plugged in?")
fi
# Auto start with dex
dex=1
if command -v dex &> /dev/null ; then
export dex=$(yesno "Would you like to autostart applications with dex?")
fi
if [[ $dex -eq 0 ]]; then
dex -t "${XDG_CONFIG_HOME:-${HOME}/.config}/autostart" -c $(command -v orca)
dex -t "${XDG_CONFIG_HOME:-${HOME}/.config}/autostart" -c $(command -v $screenReader)
fi
if command -v acpi &> /dev/null ; then
batteryAlert=1
batteryAlert=$(yesno "Do you want low battery notifications?")
fi
brlapi=1
brlapi=$(yesno "Do you want to use a braille display with Orca?")
brlapi=$(yesno "Do you want to use a braille display with ${screenReader##*/}?")
sounds=1
sounds=$(yesno "Do you want window event sounds?")
# Custom applications for ratpoison mode
addCustomApplication
if [[ -d "${i3Path}" ]]; then
yesno "This will replace your existing configuration at ${i3Path}. Do you want to continue?" || exit 0
@ -248,10 +581,20 @@ mkdir -p "${i3Path}"
cp -rv scripts/ "${i3Path}/" | dialog --backtitle "I38" --progressbox "Moving scripts into place and writing config..." -1 -1
cat << EOF > ${i3Path}/config
# Generated by I38 (${0##*/}) https://github.com/stormdragon2976/I38
# Generated by I38 (${0##*/}) https://git.stormux.org/storm/I38
# $(date '+%A, %B %d, %Y at %I:%M%p')
EOF
# If we are using Sway, we need to load in the system configuration
# Usually, this is for system specific dBus things that the distro knows how to manage; we should trust their judgment with that
if [[ $usingSway ]] && [[ -d "${swaySystemIncludesPath}" ]]; then
cat << EOF >> ${i3Path}/config
# Include your distribution Sway configuration files.
include ${swaySystemIncludesPath}/*
EOF
fi
cat << EOF >> ${i3Path}/config
# i3 config file (v4)
#
# Please see https://i3wm.org/docs/userguide.html for a complete reference!
@ -279,6 +622,9 @@ bindsym \$mod+Shift+F1 exec ${i3Path}/scripts/i38-help.sh
# Run dialog
bindsym \$mod+F2 exec ${i3Path}/scripts/run_dialog.sh
# Bookmarks dialog
bindsym \$mod+Control+b exec ${i3Path}/scripts/bookmarks.sh
# Clipboard manager
bindsym \$mod+Control+c exec clipster -s
@ -288,7 +634,7 @@ bindsym \$mod+Control+Delete exec --no-startup-id sgtk-bar
# Use pactl to adjust volume in PulseAudio.
bindsym \$mod+XF86AudioRaiseVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ +${volumeJump}% & play -qnG synth 0.03 sin 440
bindsym \$mod+XF86AudioLowerVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ -${volumeJump}% & play -qnG synth 0.03 sin 440
bindsym \$mod+XF86AudioMute exec --no-startup-id ${i3Path}/scripts/mute-unmute.sh
bindsym \$mod+XF86AudioMute exec --no-startup-id ${i3Path}/scrip/ts/mute-unmute.sh
# Music player controls
# Requires playerctl.
@ -302,16 +648,16 @@ bindsym XF86AudioStop exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2
bindsym XF86AudioNext exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2000 pad 0 .02" "| sox -np synth 0.03 sin 2000" norm 1.0 vol 0.4 & ${i3Path}/scripts/music_controler.sh next
# start a terminal
bindsym \$mod+Return exec i3-sensible-terminal
bindsym \$mod+Return exec ${i3Path}/scripts/i3-sensible-terminal.sh
# kill focused window
bindsym \$mod+F4 kill
# Applications menu
bindsym \$mod+F1 exec --no-startup-id sgtk-menu -f
bindsym \$mod+F1 exec --no-startup-id "${i3Path}/scripts/menu.py"
# Desktop icons
bindsym \$mod+Control+d exec --no-startup-id yad --icons --compact --no-buttons --title="Desktop" --close-on-unfocus --read-dir=${HOME}/Desktop
bindsym \$mod+Control+d exec --no-startup-id ${i3Path}/scripts/desktop.sh
# change focus
# alt+tab and alt+shift+tab
@ -322,9 +668,13 @@ bindsym Mod1+Tab focus right
bindsym \$mod+BackSpace fullscreen toggle
# move the currently focused window to the scratchpad
# Move the currently focused window to the scratchpad
bindsym \$mod+Shift+minus move scratchpad
# Bind the currently focused window to the scratchpad
# This means it will always open in the scratchpad
bindsym \$mod+Shift+equal exec --no-startup-id ${i3Path}/scripts/bind_to_scratchpad.sh
# Show the next scratchpad window or hide the focused scratchpad window.
# If there are multiple scratchpad windows, this command cycles through them.
bindsym \$mod+minus scratchpad show
@ -367,6 +717,63 @@ bindsym Control+Shift+F9 move container to workspace number \$ws9, exec spd-say
bindsym Control+Shift+F10 move container to workspace number \$ws10, exec spd-say -P important -Cw "moved to workspace 10"
# A mode that will pass all keys except $mod+shift+backspace to the current application.
# Use $mod+shift+backspace to exit the mode.
bindsym $mod+shift+BackSpace mode "bypass"
mode "bypass" {
# Exit bypass mode.
bindsym $mod+Shift+BackSpace mode "default"
}
EOF
# ocr through speech-dispatcher
echo "bindsym ${mod}+F5 exec ${i3Path}/scripts/ocr.py" >> ${i3Path}/config
# Interrupt speech-dispatcher output
echo "bindsym ${mod}+Shift+F5 exec spd-say -C" >> ${i3Path}/config
# Multiple keyboard layouts if requested.
if [[ ${#kbd[@]} -gt 1 ]]; then
echo "bindsym Mod4+space exec ${i3Path}/scripts/keyboard.sh cycle ${kbd[@]}" >> ${i3Path}/config
fi
# Create panel mode
cat << EOF >> ${i3Path}/config
# Panel mode configuration
bindsym Control+Mod1+Tab mode "panel"
mode "panel" {
# Weather information bound to w
bindsym w exec --no-startup-id ${i3Path}/scripts/weather.sh, mode "default"
# Magic wormhole bound to shift+W
bindsym Shift+w exec --no-startup-id ${i3Path}/scripts/wormhole.py, mode "default"
# System information bound to s
bindsym s exec --no-startup-id ${i3Path}/scripts/sysinfo.sh, mode "default"
$(if command -v remind &> /dev/null ; then
echo "# Reminders bound to r"
echo "bindsym r exec --no-startup-id ${i3Path}/scripts/reminder.sh, mode \"default\""
fi)
# Simple notes system bound to n
bindsym n exec --no-startup-id ${i3Path}/scripts/notes.py, mode "default"
$(if command -v blueman-manager &> /dev/null ; then
echo "# Bluetooth bound to b"
echo "bindsym b exec --no-startup-id blueman-manager, mode \"default\""
fi)
$(if command -v lxsession-logout &> /dev/null ; then
echo "# Power options bound to p"
echo "bindsym p exec --no-startup-id lxsession-logout, mode \"default\""
fi)
# Exit panel mode without any action
bindsym Escape mode "default"
bindsym Control+g mode "default"
}
EOF
# Create ratpoison mode if requested.
@ -377,11 +784,15 @@ mode "ratpoison" {
# I38 help bound to ?
bindsym Shift+slash exec ${i3Path}/scripts/i38-help.sh, mode "default"
# Terminal emulator bound to c
bindsym c exec i3-sensible-terminal, mode "default"
bindsym c exec $sensibleTerminal, mode "default"
# Text editor bound to e
bindsym e exec $textEditor, mode "default"
# File browser bound to f
bindsym f exec $fileBrowser, mode "default"
$(if [[ ${#fileBrowser} -gt 3 ]]; then
echo "# File browser bound to f"
echo "bindsym f exec $fileBrowser, mode \"default\""
fi)
# Email client bound to \$mod+e
bindsym \$mod+e exec $emailClient, mode "default"
# Web browser bound to w
bindsym w exec $webBrowser, mode "default"
# Kill window bound to k
@ -399,9 +810,9 @@ $(if command -v pidgin &> /dev/null ; then
echo "# p bound to pidgin"
echo "bindsym p exec $(command -v pidgin), mode \"default\""
fi)
$(if command -v transfersh &> /dev/null ; then
echo "# t bound to share file with transfer.sh"
echo 'bindsym t exec bash -c '"'"'fileName="$(yad --title "I38 Upload File" --file)" && url="$(transfersh "${fileName}" | tee >(yad --title "I38 - Uploading ${fileName##*/} ..." --progress --pulsate --auto-close))" && echo "${url#*saved at: }" | tee >(yad --title "I38 - Upload URL" --show-cursor --show-uri --button yad-close --sticky --text-info) >(xclip -selection clipboard)'"', mode \"default\""
$(if command -v xrandr &> /dev/null ; then
echo "# alt+s bound to brightness control"
echo "bindsym \$mod+s exec --no-startup-id ${i3Path}/scripts/screen_controller.sh, mode \"default\""
fi)
#Keyboard based volume Controls with pulseaudio
@ -420,15 +831,35 @@ bindsym Mod1+Shift+u exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 20
#Check battery status
bindsym Mod1+b exec --no-startup-id ${i3Path}/scripts/battery_status.sh, mode "default"
#Check controller battery status
bindsym g exec ${i3Path}/scripts/game_controler.sh -s, mode "default"
bindsym g exec ${i3Path}/scripts/game_controller.sh -s, mode "default"
# Get a list of windows in the current workspace
bindsym apostrophe exec --no-startup-id ${i3Path}/scripts/window_list.sh, mode "default"
# Restart orca
# Restart Cthulhu
bindsym Shift+c exec $(command -v cthulhu) --replace, mode "default"
# Restart Orca
bindsym Shift+o exec $(command -v orca) --replace, mode "default"
# reload the configuration file
bindsym Control+semicolon exec bash -c '$i3msg -t run_command reload && spd-say -P important -Cw "I38 Configuration reloaded."', mode "default"
# restart i3 inplace (preserves your layout/session, can be used to upgrade i3)
bindsym Control+Shift+semicolon exec bash -c '$i3msg -t run_command restart && spd-say -P important -Cw "I3 restarted."', mode "default"
# Toggle screen reader
bindsym Shift+t exec ${i3Path}/scripts/toggle_screenreader.sh, mode "default"
$(if [[ $usingSway -eq 0 ]]; then
echo "# reload the configuration file"
echo "bindsym Control+semicolon exec bash -c '$i3msg -t command reload && spd-say -P important -Cw "I38 Configuration reloaded."', mode "default""
else
echo "# reload the configuration file"
echo "bindsym Control+semicolon exec bash -c '$i3msg -t run_command reload && spd-say -P important -Cw "I38 Configuration reloaded."', mode "default""
echo "# restart i3 inplace (preserves your layout/session, can be used to upgrade i3)"
echo "bindsym Control+Shift+semicolon exec $i3msg -t run_command restart && spd-say -P important -Cw "I3 restarted.", mode "default""
fi)
# Custom applications
$(for app in "${customApps[@]}"; do
IFS='|' read -r appName appCommand appFlags appKey <<< "$app"
echo "# $appName bound to $appKey"
if [[ -n "$appFlags" ]]; then
echo "bindsym $appKey exec $appCommand $appFlags, mode \"default\""
else
echo "bindsym $appKey exec $appCommand, mode \"default\""
fi
done)
# Run dialog with exclamation
bindsym Shift+exclam exec ${i3Path}/scripts/run_dialog.sh, mode "default"
# exit i3 (logs you out of your X session)
@ -442,87 +873,102 @@ bindsym Control+g mode "default"
EOF
fi
# For those who do not want ratpoison mode.
if [[ -z "${escapeKey}" ]]; then
cat << EOF >> ${i3Path}/config
# Text editor bound to $mod+e
bindsym \$mod+e exec $textEditor
# File browser bound to $mod+f
bindsym \$mod+f exec $fileBrowser
# Web browser bound to $mod+w
bindsym \$mod+w exec $webBrowser
$(if command -v mumble &> /dev/null ; then
echo "bindsym \$mod+m exec $(command -v mumble)"
fi)
$(if command -v ocrdesktop &> /dev/null ; then
echo "bindsym Print exec $(command -v ocrdesktop)' -b"
fi)
$(if command -v pidgin &> /dev/null ; then
echo "bindsym \$mod+p exec $(command -v pidgin)"
fi)
$(if command -v transfersh &> /dev/null ; then
echo 'bindsym \$mod+t exec bash -c '"'"'fileName="$(yad --title "I38 Upload File" --file)" && url="$(transfersh "${fileName}" | tee >(yad --title "I38 - Uploading ${fileName##*/} ..." --progress --pulsate --auto-close))" && echo "${url#*saved at: }" | tee >(yad --title "I38 - Upload URL" --show-cursor --show-uri --button yad-close --sticky --text-info) >(xclip -selection clipboard)'"'"
fi)
#Keyboard based volume controls using pulseaudio
bindsym \$mod+Shift+0 exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ +${volumeJump}% & play -qnG synth 0.03 sin 440
bindsym \$mod+Shift+9 exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ -${volumeJump}% & play -qnG synth 0.03 sin 440
# Music player controls
# Requires playerctl.
bindsym \$mod+Shift+equal exec --no-startup-id ${i3Path}/scripts/music_controler.sh incvol $volumeJump
bindsym \$mod+Shift+minus exec --no-startup-id ${i3Path}/scripts/music_controler.sh decvol $volumeJump
bindsym \$mod+Shift+z exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2000 pad 0 .02" "| sox -np synth 0.03 sin 2000" norm 1.0 vol 0.4 & ${i3Path}/scripts/music_controler.sh prev
bindsym \$mod+Shift+c exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2000 pad 0 .02" "| sox -np synth 0.03 sin 2000" norm 1.0 vol 0.4 & ${i3Path}/scripts/music_controler.sh pause
bindsym \$mod+Shift+x exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2000 pad 0 .02" "| sox -np synth 0.03 sin 2000" norm 1.0 vol 0.4 & ${i3Path}/scripts/music_controler.sh play
bindsym \$mod+Shift+v exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2000 pad 0 .02" "| sox -np synth 0.03 sin 2000" norm 1.0 vol 0.4 & ${i3Path}/scripts/music_controler.sh stop
bindsym \$mod+Shift+b exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2000 pad 0 .02" "| sox -np synth 0.03 sin 2000" norm 1.0 vol 0.4 & ${i3Path}/scripts/music_controler.sh next
bindsym \$mod+Shift+u exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2000 pad 0 .02" "| sox -np synth 0.03 sin 2000" norm 1.0 vol 0.4 & ${i3Path}/scripts/music_controler.sh info
#Check battery status
bindsym \$mod+b exec --no-startup-id ${i3Path}/scripts/battery_status.sh
#Check controller battery status
bindsym \$mod+g exec ${i3Path}/scripts/game_controler.sh -s
# Get a list of windows in the current workspace
bindsym \$mod+apostrophe exec --no-startup-id ${i3Path}/scripts/window_list.sh
# Restart orca
bindsym \$mod+Shift+o exec $(command -v orca) --replace
# reload the configuration file
bindsym \$mod+Control+semicolon exec bash -c '$i3msg -t run_command reload && spd-say -P important -Cw "I38 Configuration reloaded."'
# restart i3 inplace (preserves your layout/session, can be used to upgrade i3)
bindsym \$mod+Control+Shift+semicolon exec bash -c '$i3msg -t run_command restart && spd-say -P important -Cw "I3 restarted."'
# exit i3 (logs you out of your X session)
bindsym \$mod+Control+q exec bash -c 'yad --image "dialog-question" --title "I38" --button=yes:0 --button=no:1 --text "You pressed the exit shortcut. Do you really want to exit i3? This will end your X session." && $i3msg -t run_command exit'
EOF
fi
cat << EOF >> ${i3Path}/config
# Auto start section
$(if [[ $sounds -eq 0 ]]; then
echo "exec_always --no-startup-id ${i3Path}/scripts/sound.py"
if [[ $usingSway -eq 0 ]]; then
echo "exec --no-startup-id ${i3Path}/scripts/sound.py"
else
echo "exec_always --no-startup-id ${i3Path}/scripts/sound.py"
fi
fi
if [[ $brlapi -eq 0 ]]; then
echo 'exec --no-startup-id xbrlapi --quiet'
fi
if [[ -x "/usr/lib/notification-daemon-1.0/notification-daemon" ]]; then
if [[ $udiskie -eq 0 ]]; then
echo 'exec --no-startup-id udiskie'
fi
if [[ -x "/usr/lib/xfce4/notifyd/xfce4-notifyd" ]]; then
echo 'exec_always --no-startup-id /usr/lib/xfce4/notifyd/xfce4-notifyd'
elif [[ -x "/usr/lib/notification-daemon-1.0/notification-daemon" ]]; then
echo 'exec_always --no-startup-id /usr/lib/notification-daemon-1.0/notification-daemon -r'
fi
# Work around for weird Void Linux stuff.
if [[ -x "/usr/libexec/notification-daemon" ]]; then
echo 'exec_always --no-startup-id /usr/libexec/notification-daemon -r'
fi
if command -v remind &> /dev/null && command -v notify-send &> /dev/null ; then
echo "exec_always --no-startup-id ${i3Path}/scripts/launch_remind.sh"
touch ~/.reminders
fi
if [[ $batteryAlert -eq 0 ]]; then
echo "exec_always --no-startup-id ${i3Path}/scripts/battery_alert.sh"
fi
if [[ $dex -eq 0 ]]; then
echo '# Start XDG autostart .desktop files using dex. See also'
echo '# https://wiki.archlinux.org/index.php/XDG_Autostart'
echo 'exec --no-startup-id dex --autostart --environment i3'
else
echo '# Startup applications'
echo 'exec clipster -d'
echo 'exec orca'
if command -v x11bell &> /dev/null ; then
echo 'exec --no-startup-id x11bell play -nqV0 synth .1 sq norm -12'
fi
echo 'exec --no-startup-id clipster -d'
echo "exec $screenReader"
echo "exec_always --no-startup-id ${i3Path}/scripts/desktop.sh"
fi)
# First run help documentation
exec --no-startup-id bash -c 'if [[ -f "${i3Path}/firstrun" ]]; then ${webBrowser} "${i3Path}/I38.html"& rm "${i3Path}/firstrun"; fi'
# If you want to add personal customizations to i3, add them in ${i3Path}/customizations
# It is not overwritten with the config file is recreated.
$(if [[ -r "${i3Path}/customizations" ]]; then
echo "include \"${i3Path}/customizations\""
else
echo "# Rerun the ${0##*/} script after creating the customizations file so it is detected."
fi)
# It is not overwritten when the config file is recreated.
include "${i3Path}/customizations"
EOF
touch "${i3Path}/customizations"
touch "${i3Path}/scratchpad"
# Check for markdown or pandoc for converting the welcome document
if command -v pandoc &> /dev/null ; then
pandoc -f markdown -t html "I38.md" -so "${i3Path}/I38.html" --metadata title="Welcome to I38"
elif command -v markdown &> /dev/null ; then
cat << EOF > "${i3Path}/I38.html"
<!DOCTYPE html>
<html>
<head>
<title>Welcome to I38</title>
<meta charset="utf-8">
</head>
<body>
EOF
# Convert markdown to html and append to the file
markdown "I38.md" >> "${i3Path}/I38.html"
# Close the HTML tags using heredoc
cat << EOF >> "${i3Path}/I38.html"
</body>
</html>
EOF
fi
# More readable version of variables.
escapeKey="${escapeKey//Mod1/Alt}"
escapeKey="${escapeKey//Mod4/Super}"
mod="${mod//Mod1/Alt}"
mod="${mod//Mod4/Super}"
webBrowser="${webBrowser##*/}"
screenReader="${screenReader##*/}"
textEditor="${textEditor##*/}"
fileBrowser="${fileBrowser##*/}"
# Customize the html file to the user's choices.
sed -i -e "s|BROWSER|${webBrowser}|g" \
-e "s|MODKEY|${mod}|g" \
-e "s|SCREENREADER|${screenReader}|g" \
-e "s|RATPOISONKEY|${escapeKey}|g" \
-e "s|TEXTEDITOR|${textEditor}|g" \
-e "s|FILEBROWSER|${fileBrowser}|g" "${i3Path}/I38.html"
# Create the firstrun file
touch "${i3Path}/firstrun"

2
panel.conf Normal file
View File

@ -0,0 +1,2 @@
assign [class="Solaar"] workspace number 11
assign [class="qjoypad"] workspace number 11

View File

@ -1,11 +1,36 @@
#!/usr/bin/env bash
workSpace="$(i3-msg -t get_workspaces \
| jq '.[] | select(.focused==true).name' \
| cut -d"\"" -f2)"
# This file is part of I38.
# I38 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 3 of the License, or (at your option) any later version.
# I38 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 I38. If not, see <https://www.gnu.org/licenses/>.
path="$(readlink -f $0)"
path="${path%/*/*}"
path="${path##*/}"
if [[ "$path" == "i3" ]]; then
workSpace="$(i3-msg -t get_workspaces \
| jq '.[] | select(.focused==true).name' \
| cut -d"\"" -f2)"
else
workSpace="$(swaymsg -t get_workspaces \
| jq '.[] | select(.focused==true).name' \
| cut -d"\"" -f2)"
fi
left=9
right=0
msg="Workspace ${workSpace}"
if [[ "${workSpace}" == "11" ]]; then
play -qnV0 synth 1.5 pl A4 pl E5 pl C5 delay 0.0 0.1 0.2 remix - fade p 0 1.5 .5
spd-say -P important -Cw "I38 panel"
exit 0
fi
if ! [[ "${workSpace}" =~ ^[0-9]+$ ]]; then
right=9
else

36
scripts/battery_alert.sh Executable file
View File

@ -0,0 +1,36 @@
#!/bin/sh
# This file is part of I38.
# I38 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 3 of the License, or (at your option) any later version.
# I38 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 I38. If not, see <https://www.gnu.org/licenses/>.
if ! command -v acpi &> /dev/null ; then
exit 0
fi
while : ; do
acpi -b | awk -F'[,:%]' '{print $2, $3}' | {
read -r status capacity
if [[ "$status" == "Discharging" ]] && [[ "$capacity" -le 15 ]] && [[ "$capacity" -gt 10 ]]; then
play -qV0 "|sox -n -p synth saw E2 fade 0 0.25 0.05" "|sox -n -p synth saw E2 fade 0 0.25 0.05" norm -7
spd-say -P important "Battery $capacity percent."
elif [[ "$status" == "Discharging" ]] && [[ "$capacity" -le 10 ]] && [[ "$capacity" -gt 5 ]]; then
play -qV0 "|sox -n -p synth saw E2 fade 0 0.25 0.05" "|sox -n -p synth saw E2 fade 0 0.25 0.05" norm -7
spd-say -P important "Battery $capacity percent."
elif [[ "$status" == "Discharging" ]] && [[ "$capacity" -lt 5 ]]; then
play -qV0 "|sox -np synth sq C#5 sq D#5 sq F#5 sq A5 sq C#6" remix - fade 0 5.5 5 pitch -400
spd-say -P important "Battery $capacity percent."
fi
}
sleep 5m
done
exit 0

View File

@ -1,5 +1,16 @@
#!/usr/bin/env bash
# This file is part of I38.
# I38 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 3 of the License, or (at your option) any later version.
# I38 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 I38. If not, see <https://www.gnu.org/licenses/>.
#check for acpi
if command -v acpi &> /dev/null; then
bat="$(acpi -b)"

32
scripts/bind_to_scratchpad.sh Executable file
View File

@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Find out if we're using i3
if ! [[ -n "${WAYLAND_DISPLAY}" ]]; then
cmd="i3-msg"
scratchConfig="${XDG_CONFIG_HOME:-$HOME/.config}/i3"
else
cmd="swaymsg"
scratchConfig="${XDG_CONFIG_HOME:-$HOME/.config}/sway"
fi
scratchConfig+="/scratchpad"
touch "${scratchConfig}"
# Get the focused window ID
windowId=$(xdotool getactivewindow)
# Get the class name of the window
class=$(xprop -id "$windowId" WM_CLASS | awk -F '"' '{print $4}')
if [[ -z "$class" ]]; then
notify-send "Unable to move to scratchpad."
exit 1
fi
# Check if it's already in the config
if ! grep -q "class=\"$class\"" "$scratchConfig"; then
echo "for_window [class=\"$class\"] move to scratchpad" >> "$scratchConfig"
notify-send "Added window class $class to scratchpad"
fi
# Move the window to scratchpad now
$cmd "[class=\"$class\"] move to scratchpad"

142
scripts/bookmarks.sh Executable file
View File

@ -0,0 +1,142 @@
#!/usr/bin/env bash
# This file is part of I38.
# I38 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 3 of the License, or (at your option) any later version.
# I38 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 I38. If not, see <https://www.gnu.org/licenses/>.
add_bookmark() {
while : ; do
if [[ -r "${config}/surfraw/bookmarks" ]] && [[ -r "${config}/i3/bookmarks" ]]; then
# Ask for the Bookmark Title, URL, and file selection using yad --form with radio buttons
input=$(yad --form \
--title="I38 - Add Bookmark" \
--text="New Bookmark:" \
--field="Bookmark Title" \
--field="URL" \
--field="!Save As:CB" \
--field="Surfraw Bookmark":CHK \
--field="I38 Bookmark":CHK \
--button="OK:0" \
--button="Cancel:1" \
--selectable-labels \
--separator="|")
else
# Ask for the Bookmark Title and URL using yad --form without radio buttons
input=$(yad --form \
--title="I38 - Add Bookmark" \
--text="New Bookmark:" \
--field="Bookmark Title" \
--field="URL" \
--button="OK:0" \
--button="Cancel:1" \
--selectable-labels \
--separator="|")
fi
yadCode="$?"
# Check if the user pressed Cancel
if [[ $yadCode -eq 1 ]]; then
exit 0
fi
# Split the input into components
IFS='|' read -r title url bookmarkFile <<< "$input"
# Check if title or URL is empty
if [[ -z "$title" || -z "$url" ]]; then
yad --info --title="Add Bookmark" --text="Both Title and URL cannot be empty. Please try again."
continue
fi
# Check for spaces in title and prompt user
if [[ "$title" =~ \ ]] && [[ "$bookmarkFile" == "Surfraw Bookmark" ]]; then
newTitle="${title//[[:space:]]/_}"
yad --question --title="I38 - Bookmarks" --text="The title contains spaces, which have been converted to underscores: \"$newTitle\". Is this okay?"
yadCode="$?"
if [[ $yadCode -ne 0 ]]; then
continue
else
title="$newTitle"
fi
fi
# Determine the bookmarks file based on radio button selection
if [[ -f "${config}/surfraw/bookmarks" ]] && [[ -f "${config}/i3/bookmarks" ]]; then
if [[ "$bookmarkFile" == "Surfraw Bookmark" ]]; then
bookmarksFile="${config}/surfraw/bookmarks"
else
bookmarksFile="${config}/i3/bookmarks"
fi
else
bookmarksFile="${config}/i3/bookmarks"
fi
# Check for duplicates
if grep -q "$url" "$bookmarksFile" ; then
existingTitle=$(grep "$url" "$bookmarksFile" | cut -d' ' -f1)
yad --form --title="I38 - Bookmarks" --selectable-labels --field="This site is already bookmarked as \"$existingTitle\".":LBL --button="OK:0"
return
elif grep -qi "^$title " "$bookmarksFile"; then
yad --form --title="I38 - Bookmarks" --selectable-labels --field="A bookmark with the title \"$title\" already exists.":LBL --button="OK:0"
return
fi
# Add the new bookmark
echo "$title $url" >> "$bookmarksFile"
# Ensure no duplicates with sort -u
sort -u "$bookmarksFile" -o "$bookmarksFile"
yad --form --title="I38 - Bookmarks" --selectable-labels --field="Bookmark \"$title\" added successfully.":LBL --button="OK:0"
return
done
}
# Prepare environment and read bookmarks
declare -a bookmarks
config="${XDG_CONFIG_HOME:-${HOME}/.config}"
if [[ -r "${config}/surfraw/bookmarks" ]]; then
mapfile -t contents < "${config}/surfraw/bookmarks"
for i in "${contents[@]}" ; do
title="${i% *}"
title="${title//_/ }"
bookmarks+=("${title}")
bookmarks+=("${i##* }")
done
fi
if [[ -r "${config}/i3/bookmarks" ]]; then
mapfile -t contents < "${config}/i3/bookmarks"
for i in "${contents[@]}" ; do
bookmarks+=("${i% *}" "${i##* }")
done
fi
# Run yad to display the dialog
url=$(yad --list \
--title="I38 - Bookmarks" \
--text="Select a bookmark to open" \
--column="Title" \
--column="URL" \
--button="Open Bookmark:0" \
--button="Add Bookmark:1" \
--button="Cancel:2" \
--close-on-unfocus \
--hide-column=2 \
--search-column=1 \
--skip-taskbar \
"${bookmarks[@]}")
yadCode="$?"
url="${url%|*}"
url="${url##*|}"
case ${yadCode} in
0) xdg-open "${url}";;
1) add_bookmark;;
2) exit 0;;
esac

51
scripts/desktop.sh Executable file
View File

@ -0,0 +1,51 @@
#!/usr/bin/env bash
# This file is part of I38.
# I38 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 3 of the License, or (at your option) any later version.
# I38 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 I38. If not, see <https://www.gnu.org/licenses/>.
# Make sure there is a desktop to search.
if [[ ! -d ~/Desktop ]]; then
exit 0
fi
# Discover directories and files on the desktop
mapfile -t desktopDirs < <(find ~/Desktop -mindepth 1 -maxdepth 1 -type d)
mapfile -t desktopFiles < <(find ~/Desktop -mindepth 1 -maxdepth 1 -type f)
# Combine directories and files into one list
desktopItems=("${desktopDirs[@]}" "${desktopFiles[@]}")
# Build menu for yad.
declare -a menuList
for i in "${desktopItems[@]}" ; do
case "$i" in
*".desktop")
menuList+=("$(grep "^Name" "$i" | cut -f2- -d '=')" "gio launch \"$i\"")
;;
*)
menuList+=("${i##*/}" "/usr/bin/env xdg-open \"$i\"")
;;
esac
done
launch=$(yad --list \
--title="I38 Desktop" \
--column="Items" \
--column="Launch Command" \
--close-on-unfocus \
--hide-column=2 \
--search-column=1 \
--skip-taskbar \
"${menuList[@]}")
if [[ $? -eq 0 ]]; then
launch="${launch%|}"
launch="${launch##*|}"
eval "$launch"
fi

View File

@ -1,5 +1,16 @@
#!/bin/bash
# This file is part of I38.
# I38 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 3 of the License, or (at your option) any later version.
# I38 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 I38. If not, see <https://www.gnu.org/licenses/>.
# Add this to your crontab to have battery status automatically reported when it starts getting low
# */10 * * * * XDG_RUNTIME_DIR=/run/user/1000 /home/user/.config/i3/scripts/game_controler.sh
@ -8,7 +19,7 @@
batteryName=""
if [[ "$batteryName" == "" ]]; then
batteryName="$(find /sys/class/power_supply -name 'sony_controller_battery_*' | cut -d/ -f5)"
batteryName="$(find /sys/class/power_supply -name 'sony_controller_battery_*' -or -name 'ps-controller-battery-*' | cut -d/ -f5)"
fi
# If there's no file, we don't check it.

23
scripts/i3-sensible-terminal.sh Executable file
View File

@ -0,0 +1,23 @@
#!/bin/sh
#
# This code is released in public domain by Han Boetes <han@mijncomputer.nl>
#
# This script tries to exec a terminal emulator by trying some known terminal
# emulators.
#
# We welcome patches that add distribution-specific mechanisms to find the
# preferred terminal emulator. On Debian, there is the x-terminal-emulator
# symlink for example.
#
# Invariants:
# 1. $TERMINAL must come first
# 2. Distribution-specific mechanisms come next, e.g. x-terminal-emulator
# 3. The terminal emulator with best accessibility comes first.
# 4. No order is guaranteed/desired for the remaining terminal emulators.
for terminal in "$TERMINAL" x-terminal-emulator mate-terminal gnome-terminal terminator xfce4-terminal urxvt rxvt termit Eterm aterm uxterm xterm roxterm termite lxterminal terminology st qterminal lilyterm tilix terminix konsole kitty guake tilda alacritty hyper wezterm; do
if command -v "$terminal" > /dev/null 2>&1; then
exec "$terminal" "$@"
fi
done
i3-nagbar -m 'i3-sensible-terminal could not find a terminal emulator. Please install one.'

View File

@ -1,6 +1,16 @@
#!/usr/bin/env bash
# This file is part of I38.
# I38 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 3 of the License, or (at your option) any later version.
# I38 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 I38. If not, see <https://www.gnu.org/licenses/>.
configPath="$(readlink -f $0)"
configPath="${configPath%/*/*}"

57
scripts/keyboard.sh Executable file
View File

@ -0,0 +1,57 @@
#!/usr/bin/env bash
# This file is part of I38.
# I38 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 3 of the License, or (at your option) any later version.
# I38 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 I38. If not, see <https://www.gnu.org/licenses/>.
# This script is a modified version of i3-keyboard-layout.
# Originally Copyright (c) 2018 Sergio Gil.
# https://github.com/porras/i3-keyboard-layout
# The original work was released under the MIT license.
set -e
get_kbdlayout() {
layout=$(setxkbmap -query | grep -oP 'layout:\s*\K([\w,]+)')
variant=$(setxkbmap -query | grep -oP 'variant:\s*\K(\w+)')
echo "$layout" "$variant"
}
set_kbdlayout() {
eval "array=($1)"
setxkbmap "${array[@]}" &&
spd-say -P important -Cw "${array[@]}"
}
cycle() {
current_layout=$(get_kbdlayout | xargs)
layouts=("$@" "$1") # add the first one at the end so that it cycles
index=0
while [ "${layouts[$index]}" != "$current_layout" ] && [ $index -lt "${#layouts[@]}" ]; do index=$[index +1]; done
next_index=$[index +1]
next_layout=${layouts[$next_index]}
set_kbdlayout "$next_layout"
}
subcommand="$1"
shift || exit 1
case $subcommand in
"get")
echo -n $(get_kbdlayout)
;;
"cycle")
cycle "$@"
;;
esac
exit 0

15
scripts/launch_remind.sh Executable file
View File

@ -0,0 +1,15 @@
#!/usr/bin/env bash
# This file is part of I38.
# I38 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 3 of the License, or (at your option) any later version.
# I38 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 I38. If not, see <https://www.gnu.org/licenses/>.
pgrep remind > /dev/null 2>&1 && exit 0
command remind -z '-k:${HOME}/.config/i3/scripts/reminder.sh %s &' ${HOME}/.reminders < /dev/null > /dev/null 2>&1 &

659
scripts/menu.py Executable file
View File

@ -0,0 +1,659 @@
#!/usr/bin/env python3
# This file is part of I38.
# I38 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 3 of the License, or (at your option) any later version.
# I38 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 I38. If not, see <https://www.gnu.org/licenses/>.
import os
import configparser
from pathlib import Path
from collections import defaultdict
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Atk', '1.0')
from gi.repository import Gtk, Gdk, GLib, Atk
def read_desktop_files(paths):
desktopEntries = []
for path in paths:
if not Path(path).exists():
continue
for file in Path(path).rglob('*.desktop'):
try:
config = configparser.ConfigParser(interpolation=None)
config.read(file, encoding='utf-8')
desktopEntries.append(config)
except (UnicodeDecodeError, configparser.Error):
continue
return desktopEntries
userApplicationsPath = Path.home() / '.local/share/applications'
systemApplicationsPath = Path('/usr/share/applications')
userFlatpakApplicationsPath = Path.home() / '.local/share/flatpak/exports/share/applications'
systemFlatpakApplicationsPath = Path('/var/lib/flatpak/exports/share/applications')
desktopEntries = read_desktop_files([userApplicationsPath, systemApplicationsPath, userFlatpakApplicationsPath, systemFlatpakApplicationsPath])
# Combine some of the categories
categoryMap = {
"2DGraphics": "Office",
"Accessibility": "Settings",
"Audio": "Audio and Video",
"AudioVideo": "Audio and Video",
"AudioVideoEditing": "Audio and Video",
"ActionGame": "Games",
"Building": "Development",
"Calendar": "Office",
"Chat": "Communication",
"Database": "Office",
"DesktopSettings": "Settings",
"Emulator": "Games",
"FlowChart": "Office",
"Game": "Games",
"HardwareSettings": "Settings",
"IDE": "Development",
"InstantMessaging": "Communication",
"Math": "Education",
"Midi": "Audio and Video",
"Mixer": "Audio and Video",
"Player": "Audio and Video",
"Presentation": "Office",
"Recorder": "Audio and Video",
"Science": "Education",
"Spreadsheet": "Office",
"Telephony": "Communication",
"Terminal": "Utilities",
"TerminalEmulator": "Utilities",
"TV": "Audio and Video",
"Utility": "Utilities",
"VectorGraphics": "Office",
"Video": "Audio and Video",
"WebDevelopment": "Development",
"WordProcessor": "Office",
"X-Alsa": "Audio and Video",
"X-Fedora": "Utilities",
"X-Jack": "Audio and Video",
"X-LXDE-Settings": "Settings",
"X-MATE-NetworkSettings": "Settings",
"X-MATE-PersonalSettings": "Settings",
"X-Red-Hat-Base": "Utilities",
"X-SuSE-Core-Office": "Office",
"X-XFCE": "XFCE",
"X-XFCE-HardwareSettings": "Settings",
"X-XFCE-PersonalSettings": "Settings",
"X-XFCE-SettingsDialog": "Settings",
"X-Xfce": "XFCE",
"X-Xfce-Toplevel": "XFCE",
}
# First, gather all applications by category
categoryApps = defaultdict(dict)
subcategories = defaultdict(set)
for entry in desktopEntries:
try:
# Check if NoDisplay=true is set
if entry.getboolean('Desktop Entry', 'NoDisplay', fallback=False):
continue
name = entry.get('Desktop Entry', 'Name', fallback=None)
execCommand = entry.get('Desktop Entry', 'Exec', fallback=None)
if not name or not execCommand:
continue
entryCategories = entry.get('Desktop Entry', 'Categories', fallback='').split(';')
# For applications with categories
mainCategory = None
validCategories = [cat for cat in entryCategories if cat.strip()]
if validCategories:
# Use first valid category as main
mainCategory = categoryMap.get(validCategories[0], validCategories[0])
categoryApps[mainCategory][name] = execCommand
else:
# If no category was found, add to "Other"
categoryApps["Other"][name] = execCommand
except (configparser.NoOptionError, KeyError):
continue
# Ensure we have an "All Applications" category that contains everything
allApps = {}
for category, apps in categoryApps.items():
allApps.update(apps)
categoryApps["All Applications"] = allApps
class I38_Tab_Menu(Gtk.Window):
def __init__(self):
super().__init__(title="I38 Menu")
self.set_default_size(500, 400)
self.set_border_width(10)
# Main container
self.mainBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
self.add(self.mainBox)
# Add search box at the top
self.searchBox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
# Create completion for the search entry
self.completionStore = Gtk.ListStore(str, str) # Name, Exec
self.completion = Gtk.EntryCompletion()
self.completion.set_model(self.completionStore)
self.completion.set_text_column(0)
self.completion.set_minimum_key_length(1)
self.completion.set_popup_completion(True)
self.completion.set_inline_completion(False)
self.completion.connect("match-selected", self.on_completion_match)
self.searchEntry = Gtk.Entry()
self.searchEntry.set_completion(self.completion)
self.searchEntry.set_placeholder_text("Search applications...")
self.searchEntry.connect("changed", self.on_search_changed)
self.searchEntry.connect("key-press-event", self.on_search_key_press)
self.searchBox.pack_start(self.searchEntry, True, True, 0)
# Add search button for visual users
self.searchButton = Gtk.Button.new_from_icon_name("search", Gtk.IconSize.BUTTON)
self.searchButton.connect("clicked", self.on_search_activated)
self.searchBox.pack_start(self.searchButton, False, False, 0)
self.mainBox.pack_start(self.searchBox, False, False, 0)
# Create notebook (tabbed interface)
self.notebook = Gtk.Notebook()
self.notebook.set_tab_pos(Gtk.PositionType.TOP)
self.mainBox.pack_start(self.notebook, True, True, 0)
# Flag for search mode
self.inSearchMode = False
# For incremental letter navigation
self.typedText = ""
self.typedTextTimer = None
# Add a tab for each major category
self.treeViews = {} # Store TreeViews for each tab
self.stores = {} # Store models for each tab
# Sort categories alphabetically, but ensure "All Applications" is first
sortedCategories = sorted(categoryApps.keys())
if "All Applications" in sortedCategories:
sortedCategories.remove("All Applications")
sortedCategories.insert(0, "All Applications")
# Create tabs - defer TreeView creation for performance
self.tabCategories = sortedCategories
self.createdTabs = set() # Track which tabs have been created
for i, category in enumerate(sortedCategories):
if not categoryApps[category]: # Skip empty categories
continue
# Create placeholder scrolled window
scrolledWindow = Gtk.ScrolledWindow()
scrolledWindow.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
# Create tab label
tabLabel = Gtk.Label(label=category)
# Add the tab
self.notebook.append_page(scrolledWindow, tabLabel)
# Set tab accessibility properties for screen readers
accessible = scrolledWindow.get_accessible()
accessible.set_name(f"{category} applications")
accessible.set_role(Atk.Role.LIST)
# Create first tab immediately
if sortedCategories:
self.create_tab_content(0)
# Connect notebook signals
self.notebook.connect("switch-page", self.on_switch_page)
# Connect window signals
self.connect("key-press-event", self.on_window_key_press)
self.connect("focus-out-event", self.on_focus_out)
# Add all applications to the completion store
self.populate_completion_store()
# Set accessibility properties
windowAccessible = self.get_accessible()
windowAccessible.set_name("I38 Application Menu")
windowAccessible.set_description("Tab-based application launcher menu. Press slash to search, type app name and use down arrow to navigate results. Type letters to incrementally navigate to matching applications.")
def create_tab_content(self, tabIndex):
"""Create TreeView content for a tab on demand"""
if tabIndex in self.createdTabs or tabIndex >= len(self.tabCategories):
return
category = self.tabCategories[tabIndex]
if not categoryApps[category]:
return
# Create a TreeStore for this category
store = Gtk.TreeStore(str, str) # Columns: Name, Exec
self.stores[category] = store
# Add applications to this category's store (simplified - no subcategories)
sortedApps = sorted(categoryApps[category].items())
for appName, appExec in sortedApps:
store.append(None, [appName, appExec])
# Create TreeView for this category
treeView = Gtk.TreeView(model=store)
treeView.set_headers_visible(False)
self.treeViews[category] = treeView
# Add column for application names
renderer = Gtk.CellRendererText()
column = Gtk.TreeViewColumn("Applications", renderer, text=0)
treeView.append_column(column)
# Get the scrolled window for this tab
scrolledWindow = self.notebook.get_nth_page(tabIndex)
scrolledWindow.add(treeView)
# Connect signals
treeView.connect("row-activated", self.on_row_activated)
treeView.connect("key-press-event", self.on_key_press)
# Show the new content
treeView.show()
# Mark this tab as created
self.createdTabs.add(tabIndex)
def populate_completion_store(self):
"""Populate completion store with all available applications"""
self.completionStore.clear()
# Add all applications from all categories
for tabName, appDict in categoryApps.items():
for appName, execCommand in sorted(appDict.items()):
self.completionStore.append([appName, execCommand])
def on_switch_page(self, notebook, page, pageNum):
# Create tab content if not already created
self.create_tab_content(pageNum)
# Focus the TreeView in the newly selected tab
tab = notebook.get_nth_page(pageNum)
for child in tab.get_children():
if isinstance(child, Gtk.TreeView):
child.grab_focus()
break
def on_row_activated(self, treeView, path, column):
model = treeView.get_model()
treeIter = model.get_iter(path)
execCommand = model.get_value(treeIter, 1)
if execCommand:
# Launch the application
cmdParts = execCommand.split()
# Remove field codes like %f, %F, %u, %U
cleanCmd = [p for p in cmdParts if not (p.startswith('%') and len(p) == 2)]
cleanCmd = ' '.join(cleanCmd)
# Use GLib.spawn_command_line_async for better process handling
try:
GLib.spawn_command_line_async(cleanCmd)
Gtk.main_quit()
except GLib.Error as e:
dialog = Gtk.MessageDialog(
transient_for=self,
flags=0,
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.OK,
text=f"Failed to launch application: {e.message}"
)
dialog.run()
dialog.destroy()
def on_completion_match(self, completion, model, iterator):
"""Handle when a completion item is selected"""
appName = model[iterator][0]
execCommand = model[iterator][1]
if execCommand:
# Launch the application
cmdParts = execCommand.split()
# Remove field codes like %f, %F, %u, %U
cleanCmd = [p for p in cmdParts if not (p.startswith('%') and len(p) == 2)]
cleanCmd = ' '.join(cleanCmd)
# Use GLib.spawn_command_line_async for better process handling
try:
GLib.spawn_command_line_async(cleanCmd)
Gtk.main_quit()
except GLib.Error as e:
dialog = Gtk.MessageDialog(
transient_for=self,
flags=0,
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.OK,
text=f"Failed to launch application: {e.message}"
)
dialog.run()
dialog.destroy()
return True
def on_search_changed(self, entry):
"""Handle search text changes"""
searchText = entry.get_text().lower()
self.inSearchMode = bool(searchText)
# If entering search mode, make sure "All Applications" tab is active
if self.inSearchMode and not self.notebook.get_current_page() == 0:
self.notebook.set_current_page(0)
currentTab = self.notebook.get_current_page()
tabName = list(self.treeViews.keys())[currentTab]
treeView = self.treeViews[tabName]
store = self.stores[tabName]
self.filter_applications(store, searchText)
# Update the completion model with filtered results
self.update_completion_results(searchText)
# Set accessibility announcement
if self.inSearchMode:
treeView.get_accessible().set_name(f"Search results for {searchText}")
else:
treeView.get_accessible().set_name(f"{tabName} applications")
def update_completion_results(self, searchText):
"""Update completion dropdown with matching applications"""
if not searchText:
# If empty, restore all applications
self.populate_completion_store()
return
# Clear and repopulate with filtered results
self.completionStore.clear()
for tabName, appDict in categoryApps.items():
for appName, execCommand in sorted(appDict.items()):
if searchText.lower() in appName.lower():
self.completionStore.append([appName, execCommand])
def filter_applications(self, store, searchText):
"""Show/hide applications based on search text"""
def filterRow(model, iterator):
if not self.inSearchMode:
return True
appName = model.get_value(iterator, 0).lower()
# Always show categories (rows with no exec command)
execCommand = model.get_value(iterator, 1)
if execCommand is None:
# Check if any children match
childIter = model.iter_children(iterator)
while childIter:
childName = model.get_value(childIter, 0).lower()
if searchText in childName:
return True
childIter = model.iter_next(childIter)
return False
# Show app if it matches search
return searchText in appName
for tabName, treeStore in self.stores.items():
# Replace store with filtered version
filterStore = treeStore.filter_new()
filterStore.set_visible_func(filterRow)
treeView = self.treeViews[tabName]
treeView.set_model(filterStore)
# Expand all categories in search mode
if self.inSearchMode:
treeView.expand_all()
else:
treeView.collapse_all()
def on_search_key_press(self, entry, event):
"""Handle keyboard input in search box"""
keyval = event.keyval
if keyval == Gdk.KEY_Down:
# Check if we can navigate to completion results
if self.completion.get_popup_completion() and self.inSearchMode:
# Try to activate completion popup and navigate to it
self.completion.complete()
return True
# If not in completion mode, move focus to the treeview
currentTab = self.notebook.get_current_page()
tabName = list(self.treeViews.keys())[currentTab]
treeView = self.treeViews[tabName]
treeView.grab_focus()
# Set cursor to first item
model = treeView.get_model()
iterator = model.get_iter_first()
if iterator:
path = model.get_path(iterator)
treeView.set_cursor(path)
return True
elif keyval == Gdk.KEY_Escape:
if self.searchEntry.get_text():
# Clear search if there's text
self.searchEntry.set_text("")
return True
else:
# Otherwise close the app
Gtk.main_quit()
return True
# Other keys pass through
return False
def on_search_activated(self, widget):
"""Process search when search button is clicked"""
searchText = self.searchEntry.get_text()
if searchText:
# Focus on the first result if any
currentTab = self.notebook.get_current_page()
tabName = list(self.treeViews.keys())[currentTab]
treeView = self.treeViews[tabName]
model = treeView.get_model()
# Find first visible row
iterator = model.get_iter_first()
if iterator:
path = model.get_path(iterator)
treeView.set_cursor(path)
treeView.grab_focus()
def on_key_press(self, widget, event):
keyval = event.keyval
if keyval == Gdk.KEY_Escape:
# Reset incremental typing and close app if no ongoing search
if self.typedText:
self.typedText = ""
# Announce reset
current_tab = self.notebook.get_current_page()
tab_name = list(self.treeViews.keys())[current_tab]
treeView = self.treeViews[tab_name]
treeView.get_accessible().set_name(f"Type reset. {tab_name} applications")
return True
else:
Gtk.main_quit()
return True
elif keyval == Gdk.KEY_slash:
# Forward slash activates search
self.searchEntry.grab_focus()
return True
elif keyval == Gdk.KEY_Left:
# If in a TreeView and Left key is pressed
path = widget.get_cursor()[0]
if path:
model = widget.get_model()
treeIter = model.get_iter(path)
if widget.row_expanded(path):
# Collapse the current row if it's expanded
widget.collapse_row(path)
return True
else:
# Move to parent if possible
parentIter = model.iter_parent(treeIter)
if parentIter:
parentPath = model.get_path(parentIter)
widget.set_cursor(parentPath)
return True
# If we couldn't handle it in the TreeView, try to switch tabs
currentPage = self.notebook.get_current_page()
if currentPage > 0 and not self.inSearchMode:
self.notebook.set_current_page(currentPage - 1)
return True
elif keyval == Gdk.KEY_Right:
# If in a TreeView and Right key is pressed
path = widget.get_cursor()[0]
if path:
model = widget.get_model()
treeIter = model.get_iter(path)
# Check if this row has children
if model.iter_has_child(treeIter):
if not widget.row_expanded(path):
widget.expand_row(path, False)
# Move to the first child
childPath = model.get_path(model.iter_children(treeIter))
widget.set_cursor(childPath)
return True
# If we couldn't handle it in the TreeView, try to switch tabs
currentPage = self.notebook.get_current_page()
if currentPage < self.notebook.get_n_pages() - 1 and not self.inSearchMode:
self.notebook.set_current_page(currentPage + 1)
return True
elif keyval == Gdk.KEY_Tab:
# Control tab navigation
currentPage = self.notebook.get_current_page()
if event.state & Gdk.ModifierType.SHIFT_MASK:
# Shift+Tab -> previous tab
if currentPage > 0 and not self.inSearchMode:
self.notebook.set_current_page(currentPage - 1)
else:
self.notebook.set_current_page(self.notebook.get_n_pages() - 1)
else:
# Tab -> next tab
if currentPage < self.notebook.get_n_pages() - 1 and not self.inSearchMode:
self.notebook.set_current_page(currentPage + 1)
else:
self.notebook.set_current_page(0)
return True
# Incremental letter navigation
elif Gdk.KEY_a <= keyval <= Gdk.KEY_z or Gdk.KEY_A <= keyval <= Gdk.KEY_Z:
# Cancel any pending timer
if self.typedTextTimer:
GLib.source_remove(self.typedTextTimer)
self.typedTextTimer = None
# Add the new letter to the typed text
letter = chr(keyval).lower()
self.typedText += letter
# Find item matching typed text
found = self.find_incremental_match(widget, self.typedText)
# Set timer to reset typed text after 1.5 seconds of inactivity
self.typedTextTimer = GLib.timeout_add(1500, self.reset_typed_text)
# Announce the letters being typed
current_tab = self.notebook.get_current_page()
tab_name = list(self.treeViews.keys())[current_tab]
treeView = self.treeViews[tab_name]
treeView.get_accessible().set_name(f"Typed: {self.typedText}")
return True if found else False
return False
def reset_typed_text(self):
"""Reset the typed text after timeout"""
self.typedText = ""
self.typedTextTimer = None
return False # Don't call again
def find_incremental_match(self, treeView, text):
"""Find items matching the incrementally typed text"""
if not text:
return False
model = treeView.get_model()
found = False
def search_tree(model, path, treeIter, user_data):
nonlocal found
if found:
return True # Stop iteration if already found
name = model.get_value(treeIter, 0)
if name and name.lower().startswith(text.lower()):
# Found a match
treeView.set_cursor(path)
treeView.scroll_to_cell(path, None, True, 0.5, 0.5)
found = True
return True # Stop iteration
return False # Continue iteration
# Search the entire model
model.foreach(search_tree, None)
return found
def on_window_key_press(self, widget, event):
# Handle window-level key events
keyval = event.keyval
if keyval == Gdk.KEY_Escape:
Gtk.main_quit()
return True
elif keyval == Gdk.KEY_slash:
# Forward slash activates search
self.searchEntry.grab_focus()
return True
return False
def on_focus_out(self, widget, event):
# Quit when the window loses focus
Gtk.main_quit()
window = I38_Tab_Menu()
window.connect("destroy", Gtk.main_quit)
window.show_all()
# Focus the first TreeView
firstTab = window.notebook.get_nth_page(0)
for child in firstTab.get_children():
if isinstance(child, Gtk.TreeView):
child.grab_focus()
break
Gtk.main()

View File

@ -1,6 +1,16 @@
#!/bin/bash
# This file is part of I38.
# I38 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 3 of the License, or (at your option) any later version.
# I38 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 I38. If not, see <https://www.gnu.org/licenses/>.
run_command() {
playerctl -p '%any,chromium,firefox' $1
sleep 0.25

View File

@ -1,9 +1,21 @@
#!/bin/bash
#!/usr/bin/env bash
# This file is part of I38.
# I38 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 3 of the License, or (at your option) any later version.
# I38 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 I38. If not, see <https://www.gnu.org/licenses/>.
if [ $(pamixer --get-mute) = false ]; then
spd-say -P important -Cw 'Muting!'
pamixer -t
else
pamixer -t
play -qnG synth 0.05 sin 440
play -qnGV0 synth 0.05 sin 440
spd-say -P important -Cw 'Unmuted!'
fi

823
scripts/notes.py Executable file
View File

@ -0,0 +1,823 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GLib, Gio, Gdk
import os
import datetime
import sqlite3
from pathlib import Path
class JotApp(Gtk.Application):
def __init__(self):
super().__init__(application_id="com.example.jot",
flags=Gio.ApplicationFlags.FLAGS_NONE)
# Set up data structures
self.dbPath = self.get_db_path()
self.conn = None
self.init_database()
# Initialize settings
self.expirationDays = self.get_setting("expirationDays", 0)
self.confirmDelete = self.get_setting("confirmDelete", 1)
def do_activate(self):
# Create main window when app is activated
self.window = Gtk.ApplicationWindow(application=self, title="I38 Notes")
self.window.set_default_size(500, 400)
# Connect the delete-event signal (for window close button)
self.window.connect("delete-event", self.on_window_close)
# Set up keyboard shortcuts
self.setup_actions()
# Build the main interface
self.build_ui()
# Check for expired notes
self.check_expirations()
self.window.show_all()
def on_window_close(self, window, event):
"""Handle window close event"""
# Close the database connection before quitting
if self.conn:
self.conn.close()
self.quit()
return True
def get_db_path(self):
"""Get path to the SQLite database"""
configHome = os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config'))
configDir = os.path.join(configHome, 'stormux', 'I38')
os.makedirs(configDir, exist_ok=True)
return os.path.join(configDir, 'notes.sqlite')
def init_database(self):
"""Initialize the SQLite database"""
try:
self.conn = sqlite3.connect(self.dbPath)
cursor = self.conn.cursor()
# Create notes table if it doesn't exist
cursor.execute('''
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires TIMESTAMP NULL,
locked BOOLEAN DEFAULT 0
)
''')
# Create settings table if it doesn't exist
cursor.execute('''
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)
''')
self.conn.commit()
# Initialize default settings if they don't exist
self.init_default_settings()
# Check if we need to migrate from old format
self.migrate_if_needed()
except sqlite3.Error as e:
print(f"Database error: {e}")
def init_default_settings(self):
"""Initialize default settings if they don't exist"""
defaultSettings = {
"expirationDays": "0",
"confirmDelete": "1" # 1 = enabled, 0 = disabled
}
cursor = self.conn.cursor()
for key, value in defaultSettings.items():
cursor.execute(
"INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)",
(key, value)
)
self.conn.commit()
def migrate_if_needed(self):
"""Check if we need to migrate from old format"""
# Check for old config directory
oldConfigHome = os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config'))
oldConfigDir = os.path.join(oldConfigHome, 'jot')
oldNotesFile = os.path.join(oldConfigDir, 'notes')
if os.path.exists(oldNotesFile):
try:
# Check if we already have notes in the database
cursor = self.conn.cursor()
cursor.execute("SELECT COUNT(*) FROM notes")
count = cursor.fetchone()[0]
# Only migrate if database is empty
if count == 0:
with open(oldNotesFile, 'r') as f:
for line in f:
parts = line.strip().split(': ', 1)
if len(parts) == 2:
noteNum, noteText = parts
cursor.execute(
"INSERT INTO notes (text, locked) VALUES (?, ?)",
(noteText, 0)
)
self.conn.commit()
print(f"Migrated notes from {oldNotesFile}")
except Exception as e:
print(f"Migration error: {e}")
def get_setting(self, key, default=None):
"""Get a setting from the database"""
try:
cursor = self.conn.cursor()
cursor.execute("SELECT value FROM settings WHERE key = ?", (key,))
result = cursor.fetchone()
if result:
return result[0]
else:
# Set default if not exists
self.save_setting(key, default)
return default
except sqlite3.Error:
return default
def save_setting(self, key, value):
"""Save a setting to the database"""
try:
cursor = self.conn.cursor()
cursor.execute(
"INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)",
(key, value)
)
self.conn.commit()
except sqlite3.Error as e:
print(f"Settings error: {e}")
def check_expirations(self):
"""Check and remove expired notes"""
try:
cursor = self.conn.cursor()
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Find expired notes that aren't locked
cursor.execute(
"SELECT id, text FROM notes WHERE expires IS NOT NULL AND expires < ? AND locked = 0",
(now,)
)
expired = cursor.fetchall()
if expired:
# Delete expired notes
cursor.execute(
"DELETE FROM notes WHERE expires IS NOT NULL AND expires < ? AND locked = 0",
(now,)
)
self.conn.commit()
expiredCount = len(expired)
if expiredCount > 0:
self.show_status_message(f"Removed {expiredCount} expired notes")
# Refresh notes list
self.populate_notes()
except sqlite3.Error as e:
print(f"Expiration check error: {e}")
def build_ui(self):
"""Build the main user interface with tabs"""
# Create notebook (tabbed interface)
self.notebook = Gtk.Notebook()
self.notebook.set_tab_pos(Gtk.PositionType.TOP)
# Make tabs keyboard navigable
self.notebook.set_can_focus(True)
self.window.add(self.notebook)
# Build notes tab
notesTab = self.build_notes_tab()
notesTabLabel = Gtk.Label(label="Notes")
self.notebook.append_page(notesTab, notesTabLabel)
self.notebook.set_tab_reorderable(notesTab, False)
# Build settings tab
settingsTab = self.build_settings_tab()
settingsTabLabel = Gtk.Label(label="Settings")
self.notebook.append_page(settingsTab, settingsTabLabel)
self.notebook.set_tab_reorderable(settingsTab, False)
# Connect tab change signal
self.notebook.connect("switch-page", self.on_tab_switched)
def build_notes_tab(self):
"""Build the notes tab"""
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
vbox.set_border_width(10)
# Notes list with scrolling
scrolled = Gtk.ScrolledWindow()
scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
# Create list store and view
self.notesStore = Gtk.ListStore(int, str, bool, str) # id, text, locked, expiry
self.notesView = Gtk.TreeView(model=self.notesStore)
self.notesView.set_activate_on_single_click(False)
self.notesView.connect("row-activated", self.on_row_activated)
# Improve keyboard navigation in the tree view
self.notesView.set_can_focus(True)
self.notesView.set_headers_clickable(True)
self.notesView.set_enable_search(True)
self.notesView.set_search_column(1) # Search by note text
# Add columns with renderers
self.add_columns()
# Populate the list
self.populate_notes()
scrolled.add(self.notesView)
vbox.pack_start(scrolled, True, True, 0)
# Action buttons
actionBox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
# Copy button
copyButton = Gtk.Button.new_with_label("Copy to Clipboard")
copyButton.connect("clicked", self.on_copy_clicked)
copyButton.set_can_focus(True)
actionBox.pack_start(copyButton, False, False, 0)
# Delete button
deleteButton = Gtk.Button.new_with_label("Delete Note")
deleteButton.connect("clicked", self.on_delete_button_clicked)
deleteButton.set_can_focus(True)
actionBox.pack_start(deleteButton, False, False, 0)
vbox.pack_start(actionBox, False, False, 0)
# Entry for adding new notes
entryBox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
self.newNoteEntry = Gtk.Entry()
self.newNoteEntry.set_placeholder_text("Type a new note and press Enter")
self.newNoteEntry.connect("activate", self.on_entry_activate)
self.newNoteEntry.set_can_focus(True)
entryBox.pack_start(self.newNoteEntry, True, True, 0)
# Add button
addButton = Gtk.Button.new_with_label("Add Note")
addButton.connect("clicked", self.on_add_clicked)
addButton.set_can_focus(True)
entryBox.pack_start(addButton, False, False, 0)
vbox.pack_start(entryBox, False, False, 0)
# Status bar
self.statusbar = Gtk.Statusbar()
self.statusbarCtx = self.statusbar.get_context_id("jot")
vbox.pack_start(self.statusbar, False, False, 0)
return vbox
def build_settings_tab(self):
"""Build the settings tab"""
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
vbox.set_border_width(20)
# Create a frame for expiration settings
expiryFrame = Gtk.Frame(label="Note Expiration")
vbox.pack_start(expiryFrame, False, False, 0)
# Container for frame content
expiryBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
expiryBox.set_border_width(10)
expiryFrame.add(expiryBox)
# Note expiration setting
# First radio button for "Never expire"
self.neverExpireRadio = Gtk.RadioButton.new_with_label_from_widget(None, "Never expire notes")
self.neverExpireRadio.set_can_focus(True)
expiryBox.pack_start(self.neverExpireRadio, False, False, 0)
# Container for expiration days selection
expireDaysBox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
# Radio button for custom expiration
self.customExpireRadio = Gtk.RadioButton.new_with_label_from_widget(
self.neverExpireRadio,
"Expire notes after"
)
self.customExpireRadio.set_can_focus(True)
expireDaysBox.pack_start(self.customExpireRadio, False, False, 0)
# Spin button for days
adjustment = Gtk.Adjustment(
value=max(1, int(self.expirationDays)) if int(self.expirationDays) > 0 else 7,
lower=1,
upper=30,
step_increment=1
)
self.daysSpinButton = Gtk.SpinButton()
self.daysSpinButton.set_adjustment(adjustment)
self.daysSpinButton.set_can_focus(True)
expireDaysBox.pack_start(self.daysSpinButton, False, False, 0)
# Label for "days"
daysLabel = Gtk.Label(label="days")
expireDaysBox.pack_start(daysLabel, False, False, 0)
expiryBox.pack_start(expireDaysBox, False, False, 0)
# Create a frame for confirmation settings
confirmFrame = Gtk.Frame(label="Confirmations")
vbox.pack_start(confirmFrame, False, False, 10)
# Container for confirmation settings
confirmBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
confirmBox.set_border_width(10)
confirmFrame.add(confirmBox)
# Delete confirmation checkbox
self.confirmDeleteCheck = Gtk.CheckButton(label="Confirm before deleting notes")
self.confirmDeleteCheck.set_active(bool(int(self.confirmDelete)))
self.confirmDeleteCheck.set_can_focus(True)
confirmBox.pack_start(self.confirmDeleteCheck, False, False, 0)
# Set the active radio button based on current setting
if int(self.expirationDays) > 0:
self.customExpireRadio.set_active(True)
else:
self.neverExpireRadio.set_active(True)
# Connect signals
self.neverExpireRadio.connect("toggled", self.on_expiry_radio_toggled)
self.customExpireRadio.connect("toggled", self.on_expiry_radio_toggled)
# Enable/disable the spin button based on the selected radio
self.on_expiry_radio_toggled(None)
# Save button
saveButton = Gtk.Button.new_with_label("Save Settings")
saveButton.connect("clicked", self.on_save_settings)
saveButton.set_can_focus(True)
vbox.pack_start(saveButton, False, False, 10)
return vbox
def add_columns(self):
"""Add columns to the TreeView"""
# ID Column
renderer = Gtk.CellRendererText()
column = Gtk.TreeViewColumn("ID", renderer, text=0)
column.set_sort_column_id(0)
self.notesView.append_column(column)
# Note Text Column
renderer = Gtk.CellRendererText()
renderer.set_property("ellipsize", True)
column = Gtk.TreeViewColumn("Note", renderer, text=1)
column.set_expand(True)
self.notesView.append_column(column)
# Locked Column
renderer = Gtk.CellRendererToggle()
renderer.connect("toggled", self.on_locked_toggled)
column = Gtk.TreeViewColumn("Locked", renderer, active=2)
self.notesView.append_column(column)
# Expiration Column
renderer = Gtk.CellRendererText()
column = Gtk.TreeViewColumn("Expires", renderer, text=3)
self.notesView.append_column(column)
def populate_notes(self):
"""Populate the list store with notes from the database"""
self.notesStore.clear()
try:
cursor = self.conn.cursor()
cursor.execute(
"SELECT id, text, locked, expires FROM notes ORDER BY id"
)
notes = cursor.fetchall()
for note in notes:
noteId, text, locked, expires = note
# Format expiry date if it exists
expiryText = ""
if expires:
try:
expiryDate = datetime.datetime.strptime(expires, "%Y-%m-%d %H:%M:%S")
expiryText = expiryDate.strftime("%Y-%m-%d")
except:
expiryText = "Invalid date"
self.notesStore.append([
noteId, # Database ID
text, # Note text
bool(locked), # Locked status
expiryText # Expiry date
])
except sqlite3.Error as e:
print(f"Error populating notes: {e}")
def setup_actions(self):
"""Set up keyboard shortcuts"""
# Delete selected note
deleteAction = Gio.SimpleAction.new("delete", None)
deleteAction.connect("activate", self.on_delete_clicked)
self.add_action(deleteAction)
self.set_accels_for_action("app.delete", ["Delete"])
# Toggle lock on selected note
lockAction = Gio.SimpleAction.new("lock", None)
lockAction.connect("activate", self.on_lock_toggled)
self.add_action(lockAction)
self.set_accels_for_action("app.lock", ["l"])
# Copy note to clipboard
copyAction = Gio.SimpleAction.new("copy", None)
copyAction.connect("activate", self.on_copy_clicked)
self.add_action(copyAction)
self.set_accels_for_action("app.copy", ["<Control>c"])
# Switch to notes tab
notesTabAction = Gio.SimpleAction.new("notes_tab", None)
notesTabAction.connect("activate", lambda a, p: self.notebook.set_current_page(0))
self.add_action(notesTabAction)
self.set_accels_for_action("app.notes_tab", ["<Alt>1"])
# Switch to settings tab
settingsTabAction = Gio.SimpleAction.new("settings_tab", None)
settingsTabAction.connect("activate", lambda a, p: self.notebook.set_current_page(1))
self.add_action(settingsTabAction)
self.set_accels_for_action("app.settings_tab", ["<Alt>2"])
# Quit application
quitAction = Gio.SimpleAction.new("quit", None)
quitAction.connect("activate", lambda a, p: self.quit())
self.add_action(quitAction)
self.set_accels_for_action("app.quit", ["Escape"])
def show_status_message(self, message):
"""Show a message in the statusbar"""
self.statusbar.push(self.statusbarCtx, message)
# Auto-remove after 5 seconds
GLib.timeout_add_seconds(5, self.statusbar.pop, self.statusbarCtx)
def on_tab_switched(self, notebook, page, page_num):
"""Handler for tab switching"""
# Reset status bar on tab switch
self.statusbar.pop(self.statusbarCtx)
# Set focus appropriately
if page_num == 0: # Notes tab
self.newNoteEntry.grab_focus()
elif page_num == 1: # Settings tab
if self.neverExpireRadio.get_active():
self.neverExpireRadio.grab_focus()
else:
self.customExpireRadio.grab_focus()
def on_expiry_radio_toggled(self, widget):
"""Handler for expiry radio button toggles"""
# Enable/disable spin button based on which radio is active
self.daysSpinButton.set_sensitive(self.customExpireRadio.get_active())
def on_save_settings(self, button):
"""Handler for Save Settings button"""
if self.neverExpireRadio.get_active():
self.expirationDays = 0
else:
self.expirationDays = self.daysSpinButton.get_value_as_int()
# Get delete confirmation setting
self.confirmDelete = 1 if self.confirmDeleteCheck.get_active() else 0
# Save to database
self.save_setting("expirationDays", self.expirationDays)
self.save_setting("confirmDelete", self.confirmDelete)
# Apply expiration to notes if needed
if self.expirationDays > 0:
try:
cursor = self.conn.cursor()
now = datetime.datetime.now()
expiryDate = now + datetime.timedelta(days=self.expirationDays)
expiryStr = expiryDate.strftime("%Y-%m-%d %H:%M:%S")
# Set expiration for notes that aren't locked and don't have expiration
cursor.execute(
"UPDATE notes SET expires = ? WHERE locked = 0 AND expires IS NULL",
(expiryStr,)
)
self.conn.commit()
# Refresh the notes list
self.populate_notes()
except sqlite3.Error as e:
print(f"Error updating expirations: {e}")
self.show_status_message("Settings saved")
# Switch back to notes tab
self.notebook.set_current_page(0)
def on_entry_activate(self, entry):
"""Handle Enter key in the entry field"""
self.add_new_note(entry.get_text())
entry.set_text("")
def on_add_clicked(self, button):
"""Handle Add Note button click"""
self.add_new_note(self.newNoteEntry.get_text())
self.newNoteEntry.set_text("")
def add_new_note(self, text):
"""Add a new note to the database"""
if not text.strip():
self.show_status_message("Note text cannot be empty")
return
try:
cursor = self.conn.cursor()
# Set expiration if enabled
expires = None
if int(self.expirationDays) > 0:
expiryDate = datetime.datetime.now() + datetime.timedelta(days=int(self.expirationDays))
expires = expiryDate.strftime("%Y-%m-%d %H:%M:%S")
# Insert the new note
cursor.execute(
"INSERT INTO notes (text, expires, locked) VALUES (?, ?, ?)",
(text, expires, 0)
)
self.conn.commit()
# Refresh the notes list
self.populate_notes()
self.show_status_message("Note added")
except sqlite3.Error as e:
self.show_status_message(f"Error adding note: {e}")
def on_row_activated(self, view, path, column):
"""Handle double-click on a note - edit the note"""
model = view.get_model()
noteId = model[path][0]
# Get the note from the database
try:
cursor = self.conn.cursor()
cursor.execute("SELECT text, locked, expires FROM notes WHERE id = ?", (noteId,))
note = cursor.fetchone()
if note:
self.edit_note_dialog(noteId, note)
except sqlite3.Error as e:
self.show_status_message(f"Error retrieving note: {e}")
def on_locked_toggled(self, renderer, path):
"""Handle toggling the locked state from the view"""
model = self.notesView.get_model()
noteId = model[path][0]
currentLocked = model[path][2]
newLocked = not currentLocked
try:
cursor = self.conn.cursor()
# Update locked status
cursor.execute(
"UPDATE notes SET locked = ? WHERE id = ?",
(1 if newLocked else 0, noteId)
)
# If unlocking and expiration is enabled, set expiration
if not newLocked and int(self.expirationDays) > 0:
expiryDate = datetime.datetime.now() + datetime.timedelta(days=int(self.expirationDays))
expiryStr = expiryDate.strftime("%Y-%m-%d %H:%M:%S")
cursor.execute(
"UPDATE notes SET expires = ? WHERE id = ?",
(expiryStr, noteId)
)
# Update the expiry text in the model
model[path][3] = expiryDate.strftime("%Y-%m-%d")
self.show_status_message("Note unlocked - expiration set")
elif newLocked:
self.show_status_message("Note locked - will not expire")
else:
self.show_status_message("Note unlocked")
# Update the model
model[path][2] = newLocked
self.conn.commit()
except sqlite3.Error as e:
self.show_status_message(f"Error updating note: {e}")
def on_lock_toggled(self, action, parameter):
"""Handle keyboard shortcut to toggle lock"""
selection = self.notesView.get_selection()
model, treeiter = selection.get_selected()
if treeiter:
noteId = model[treeiter][0]
currentLocked = model[treeiter][2]
# Simulate clicking the toggle
path = model.get_path(treeiter)
self.on_locked_toggled(None, path)
def on_copy_clicked(self, action=None, parameter=None):
"""Copy the selected note to clipboard"""
selection = self.notesView.get_selection()
model, treeiter = selection.get_selected()
if treeiter:
noteText = model[treeiter][1]
# Get the clipboard
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
clipboard.set_text(noteText, -1)
self.show_status_message("Note copied to clipboard")
else:
self.show_status_message("No note selected")
def on_delete_button_clicked(self, button):
"""Handle Delete button click"""
selection = self.notesView.get_selection()
model, treeiter = selection.get_selected()
if treeiter:
noteId = model[treeiter][0]
noteText = model[treeiter][1]
if int(self.confirmDelete):
self.confirm_delete_note(noteId, noteText)
else:
self.delete_note(noteId)
def on_delete_clicked(self, action, parameter):
"""Handle Delete key to remove a note"""
selection = self.notesView.get_selection()
model, treeiter = selection.get_selected()
if treeiter:
noteId = model[treeiter][0]
noteText = model[treeiter][1]
if int(self.confirmDelete):
self.confirm_delete_note(noteId, noteText)
else:
self.delete_note(noteId)
def delete_note(self, noteId):
"""Delete a note by ID without confirmation"""
try:
cursor = self.conn.cursor()
cursor.execute("DELETE FROM notes WHERE id = ?", (noteId,))
self.conn.commit()
self.populate_notes()
self.show_status_message("Note deleted")
except sqlite3.Error as e:
self.show_status_message(f"Error deleting note: {e}")
def confirm_delete_note(self, noteId, noteText):
"""Show confirmation dialog before deleting a note"""
if len(noteText) > 30:
noteText = noteText[:30] + "..."
dialog = Gtk.MessageDialog(
transient_for=self.window,
flags=0,
message_type=Gtk.MessageType.QUESTION,
buttons=Gtk.ButtonsType.YES_NO,
text=f"Delete note: {noteText}?"
)
dialog.set_default_response(Gtk.ResponseType.NO)
response = dialog.run()
if response == Gtk.ResponseType.YES:
self.delete_note(noteId)
dialog.destroy()
def edit_note_dialog(self, noteId, note):
"""Show dialog to edit a note"""
text, locked, expires = note
dialog = Gtk.Dialog(
title="Edit Note",
parent=self.window,
flags=0,
buttons=(
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_SAVE, Gtk.ResponseType.OK
)
)
dialog.set_default_size(400, 200)
# Make the dialog accessible
dialog.set_role("dialog")
dialog.set_property("has-tooltip", True)
# Create a text view for the note
entryBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
entryBox.set_border_width(10)
label = Gtk.Label(label="Edit note text:")
label.set_halign(Gtk.Align.START)
entryBox.pack_start(label, False, False, 0)
# Scrolled window for text view
scrolled = Gtk.ScrolledWindow()
scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scrolled.set_shadow_type(Gtk.ShadowType.IN)
# Text view for multi-line editing
textBuffer = Gtk.TextBuffer()
textBuffer.set_text(text)
textView = Gtk.TextView.new_with_buffer(textBuffer)
textView.set_wrap_mode(Gtk.WrapMode.WORD)
textView.set_can_focus(True)
scrolled.add(textView)
entryBox.pack_start(scrolled, True, True, 0)
# Add lock checkbox
lockCheck = Gtk.CheckButton(label="Lock note (prevent expiration)")
lockCheck.set_active(bool(locked))
lockCheck.set_can_focus(True)
entryBox.pack_start(lockCheck, False, False, 0)
# Show expiration date if it exists
if expires and not locked:
try:
expiryDate = datetime.datetime.strptime(expires, "%Y-%m-%d %H:%M:%S")
expiryLabel = Gtk.Label(label=f"Expires on: {expiryDate.strftime('%Y-%m-%d')}")
expiryLabel.set_halign(Gtk.Align.START)
entryBox.pack_start(expiryLabel, False, False, 0)
except:
pass
dialog.get_content_area().add(entryBox)
dialog.set_default_response(Gtk.ResponseType.OK)
dialog.show_all()
# Set focus to text view
textView.grab_focus()
response = dialog.run()
if response == Gtk.ResponseType.OK:
# Get the text from the buffer
start, end = textBuffer.get_bounds()
newText = textBuffer.get_text(start, end, False)
newLocked = lockCheck.get_active()
try:
cursor = self.conn.cursor()
# Update the note
cursor.execute(
"UPDATE notes SET text = ?, locked = ? WHERE id = ?",
(newText, 1 if newLocked else 0, noteId)
)
# Update expiration if needed
if not newLocked and int(self.expirationDays) > 0:
expiryDate = datetime.datetime.now() + datetime.timedelta(days=int(self.expirationDays))
expiryStr = expiryDate.strftime("%Y-%m-%d %H:%M:%S")
cursor.execute(
"UPDATE notes SET expires = ? WHERE id = ?",
(expiryStr, noteId)
)
self.conn.commit()
self.populate_notes()
self.show_status_message("Note updated")
except sqlite3.Error as e:
self.show_status_message(f"Error updating note: {e}")
dialog.destroy()
def main():
app = JotApp()
return app.run(None)
if __name__ == "__main__":
main()

168
scripts/ocr.py Executable file
View File

@ -0,0 +1,168 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# This file is part of I38.
# I38 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 3 of the License, or (at your option) any later version.
# I38 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 I38. If not, see <https://www.gnu.org/licenses/>.
"""
Simple OCR Screen Reader
A lightweight tool that performs OCR on the screen and speaks the results
"""
import os
import sys
import time
import subprocess
from PIL import Image, ImageOps
import pytesseract
import pyperclip
def capture_screen(max_retries=3, initial_delay=0.2):
"""
Capture the screen using scrot with robust checking and retries
Args:
max_retries: Maximum number of attempts to read the image
initial_delay: Initial delay in seconds (will increase with retries)
"""
temp_file = "/tmp/ocr_capture.png"
try:
# Capture the screen
subprocess.run(["scrot", temp_file], check=True)
# Wait and retry approach with validity checking
delay = initial_delay
for attempt in range(max_retries):
time.sleep(delay)
# Check if file exists and has content
if os.path.exists(temp_file) and os.path.getsize(temp_file) > 0:
try:
# Try to verify the image is valid
with Image.open(temp_file) as test_img:
# Just accessing a property forces PIL to validate the image
test_img.size
# If we get here, the image is valid
return Image.open(temp_file)
except (IOError, OSError) as e:
# Image exists but isn't valid yet
if attempt < max_retries - 1:
# Increase delay exponentially for next attempt
delay *= 2
continue
else:
raise Exception(f"Image file exists but is not valid after {max_retries} attempts")
# File doesn't exist or is empty
if attempt < max_retries - 1:
# Increase delay exponentially for next attempt
delay *= 2
else:
raise Exception(f"Screenshot file not created properly after {max_retries} attempts")
except Exception as e:
print(f"Error capturing screen: {e}")
raise
finally:
# Ensure file is removed even if an error occurs
if os.path.exists(temp_file):
os.remove(temp_file)
def process_image(img, scale_factor=1.5):
"""Process the image to improve OCR accuracy"""
# Scale the image to improve OCR
if scale_factor != 1:
width, height = img.size
img = img.resize((int(width * scale_factor), int(height * scale_factor)),
Image.Resampling.BICUBIC)
# Convert to grayscale for faster processing
img = ImageOps.grayscale(img)
# Improve contrast for better text recognition
img = ImageOps.autocontrast(img)
return img
def perform_ocr(img, lang='eng'):
"""Perform OCR on the image"""
# Use tessaract with optimized settings
# --oem 1: Use LSTM OCR Engine
# --psm 6: Assume a single uniform block of text
text = pytesseract.image_to_string(img, lang=lang, config='--oem 1 --psm 6')
return text
def copy_to_clipboard(text):
"""Copy text to clipboard using pyperclip"""
try:
# Filter out empty lines and clean up the text
lines = [line.strip() for line in text.split('\n') if line.strip()]
cleaned_text = '\n'.join(lines) # Preserve line breaks for clipboard
if cleaned_text:
pyperclip.copy(cleaned_text)
return True
else:
return False
except Exception as e:
print(f"Error copying to clipboard: {e}")
return False
def speak_text(text):
"""Speak the text using speech-dispatcher"""
# Filter out empty lines and clean up the text
lines = [line.strip() for line in text.split('\n') if line.strip()]
cleaned_text = ' '.join(lines)
# Use speech-dispatcher to speak the text
if cleaned_text:
subprocess.run(["spd-say", "-Cw", cleaned_text])
else:
subprocess.run(["spd-say", "-Cw", "No text detected"])
def main():
# Limit tesseract thread usage to improve performance on Pi
os.environ["OMP_THREAD_LIMIT"] = "4"
try:
# Announce start
subprocess.run(["spd-say", "-Cw", "performing OCR"])
# Capture screen
img = capture_screen()
# Process image
processed_img = process_image(img, scale_factor=1.5)
# Perform OCR
text = perform_ocr(processed_img)
# Copy to clipboard
clipboard_success = copy_to_clipboard(text)
# Speak the results
speak_text(text)
except Exception as e:
# Let the user know something went wrong
error_msg = f"Error during OCR: {str(e)}"
print(error_msg)
try:
subprocess.run(["spd-say", "-Cw", "OCR failed"])
except:
# If even speech fails, at least we tried
pass
if __name__ == "__main__":
main()

387
scripts/reminder.sh Executable file
View File

@ -0,0 +1,387 @@
#!/usr/bin/env bash
# This file is part of I38.
# I38 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 3 of the License, or (at your option) any later version.
# I38 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 I38. If not, see <https://www.gnu.org/licenses/>.
error() {
yad --form --selectable-labels --title "I38 - Reminder Error" --field="${*}":lbl --button="Close!gtk-ok":0
}
message() {
yad --form --selectable-labels --title "I38 - Reminder" --field="${*}":lbl --button="Close!gtk-ok":0
}
add_reminder() {
info="$(yad --form --selectable-labels \
--title "I38 - New Reminder" \
--field="Comment for ~/.reminders file":lbl "" \
--field="Reminder Comment" "# Added by I38" \
--field="Enter date in yyyy-mm-dd format:":lbl "" \
--field="Date" "$(date '+%Y-%m-%d')" \
--field="Reminder text:":lbl "" \
--field="Reminder" "" \
--field="Select Hour:":num '1!1..12' \
--field="Select Minute:":num '0!0..59' \
--field="Select AM or PM":cb 'AM!PM' \
--button="Cancel!gtk-cancel:1" \
--button="Create Reminder!gtk-ok:0")"
if [[ $? -eq 1 || $? -eq 252 ]]; then
return
fi
while [[ $info =~ \|\| ]]; do
info="${info//||/|}"
done
info="${info#|#}"
# Get information for reminder into an array
IFS='|' read -a reminder <<< $info
# Fix time to be 2 digits.
[[ ${#reminder[3]} -eq 1 ]] && reminder[3]="0${reminder[3]}"
[[ ${#reminder[4]} -eq 1 ]] && reminder[4]="0${reminder[4]}"
# Check date format
if ! [[ "${reminder[1]}" =~ ^[2-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$ ]]; then
error "Invalid date format given, addition canceled."
return
fi
if [[ ${#reminder[2]} -lt 3 ]]; then
error "No reminder text given, addition canceled."
return
fi
# Add reminder
if [[ "${reminder[0]}" != "# " ]]; then
echo "# ${reminder[0]}" >> ~/.reminders
fi
echo "REM $(date -d "${reminder[1]}" '+%b %d %Y') AT ${reminder[3]}:${reminder[4]}${reminder[5]} +5 MSG ${reminder[2]} %2." >> ~/.reminders
if [[ -N ~/.reminders ]]; then
message "Reminder added."
else
error "Something went wrong. The reminder was not added."
fi
}
add_custom_reminder() {
info="$(yad --form --selectable-labels \
--title "I38 - New Custom Reminder" \
--field="Comment for ~/.reminders file":lbl "" \
--field="Reminder Comment" "" \
--field="Reminder entry. Remember for notifications, it must ccontain the +5 after the time and the %2. at the end of the MSG.":lbl "" \
--field="Reminder" "REM at +5 MSG %2.")"
if [[ $? -eq 1 || $? -eq 252 ]]; then
return
fi
while [[ $info =~ \|\| ]]; do
info="${info//||/|}"
done
info="${info#|#}"
# Get information for reminder into an array
IFS='|' read -a reminder <<< $info
if [[ "${reminder[0]}" != "# " ]]; then
echo "# ${reminder[0]}" >> ~/.reminders
fi
if [[ "${reminder[1]}" != "REM at +5 MSG %2." ]]; then
echo "${reminder[1]}" >> ~/.reminders
message "Custom reminder added."
else
error "No reminder text entered, No action taken."
fi
}
add_daily_reminder() {
info="$(yad --form --selectable-labels \
--title "I38 - New Daily Reminder" \
--field="Reminder Text" "" \
--field="Select Hour:":num '1!1..12' \
--field="Select Minute:":num '0!0..59' \
--field="Select AM or PM":cb 'AM!PM' \
--button="Cancel!gtk-cancel:1" \
--button="Create Reminder!gtk-ok:0")"
# Properly handle window close events.
if [[ $? -eq 1 || $? -eq 252 ]]; then
return
fi
# Get information for reminder into an array
IFS='|' read -a reminder <<< $info
# Fix time to be 2 digits.
[[ ${#reminder[1]} -eq 1 ]] && reminder[1]="0${reminder[1]}"
[[ ${#reminder[2]} -eq 1 ]] && reminder[2]="0${reminder[2]}"
# Make sure we have reminder text
if [[ ${#reminder[0]} -lt 3 ]]; then
error "No reminder text given, addition canceled."
return
fi
reminderEntry="REM AT ${reminder[1]}:${reminder[2]}${reminder[3]} +5 REPEAT daily MSG ${reminder[0]} %2."
echo "# Added by I38." >> ~/.reminders
echo "$reminderEntry" >> ~/.reminders
if [[ -N ~/.reminders ]]; then
message "Reminder added."
else
error "Something went wrong. The reminder was not added."
fi
}
add_monthly_reminder() {
info="$(yad --form --selectable-labels \
--title "I38 - New Monthly Reminder" \
--field="For dates over 28, some months may be skipped. If you want to be sure of the last day of the month, check the last day of month box.":lbl "" \
--field="Reminder Text" "" \
--field="Select Date:":num '1!1..31' \
--field="Select Hour:":num '1!1..12' \
--field="Select Minute:":num '0!0..59' \
--field="Select AM or PM":cb 'AM!PM' \
--field="Last day of month":chk "FALSE" \
--button="Cancel!gtk-cancel:1" \
--button="Create Reminder!gtk-ok:0")"
# Properly handle window close events.
if [[ $? -eq 1 || $? -eq 252 ]]; then
return
fi
# Get information for reminder into an array
IFS='|' read -a reminder <<< $info
# Fix time and date to be 2 digits.
[[ ${#reminder[2]} -eq 1 ]] && reminder[2]="0${reminder[2]}"
[[ ${#reminder[3]} -eq 1 ]] && reminder[3]="0${reminder[3]}"
[[ ${#reminder[4]} -eq 1 ]] && reminder[4]="0${reminder[4]}"
# Make sure we have reminder text
if [[ ${#reminder[1]} -lt 3 ]]; then
error "No reminder text given, addition canceled."
return
fi
if [[ "${reminder[6]}" == "FALSE" ]]; then
reminderEntry="REM ${reminder[2]} "
else
reminderEntry="REM 1 -1 "
fi
reminderEntry+="AT ${reminder[3]}:${reminder[4]}${reminder[5]} +5 REPEAT monthly MSG ${reminder[1]} %2."
echo "# Added by I38." >> ~/.reminders
echo "$reminderEntry" >> ~/.reminders
if [[ -N ~/.reminders ]]; then
message "Reminder added."
else
error "Something went wrong. The reminder was not added."
fi
}
add_weekly_reminder() {
info="$(yad --form --selectable-labels \
--title "I38 - New Weekly Reminder" \
--field="Reminder Text" "" \
--field="Select Days":lbl "" \
--field="Sunday":chk "FALSE" \
--field="Monday":chk "FALSE" \
--field="Tuesday":chk "FALSE" \
--field="Wednesday":chk "FALSE" \
--field="Thursday":chk "FALSE" \
--field="Friday":chk "FALSE" \
--field="Saturday":chk "FALSE" \
--field="Select Hour:":num '1!1..12' \
--field="Select Minute:":num '0!0..59' \
--field="Select AM or PM":cb 'AM!PM' \
--button="Cancel!gtk-cancel:1" \
--button="Create Reminder!gtk-ok:0")"
# Properly handle window close events.
if [[ $? -eq 1 || $? -eq 252 ]]; then
return
fi
# Get information for reminder into an array
IFS='|' read -a reminder <<< $info
# Fix time to be 2 digits.
[[ ${#reminder[9]} -eq 1 ]] && reminder[9]="0${reminder[9]}"
[[ ${#reminder[10]} -eq 1 ]] && reminder[10]="0${reminder[10]}"
# Change checked days into their name.
reminder[2]="${reminder[2]/TRUE/Sun}"
reminder[3]="${reminder[3]/TRUE/Mon}"
reminder[4]="${reminder[4]/TRUE/Tue}"
reminder[5]="${reminder[5]/TRUE/Wed}"
reminder[6]="${reminder[6]/TRUE/Thu}"
reminder[7]="${reminder[7]/TRUE/Fri}"
reminder[8]="${reminder[8]/TRUE/Sat}"
# Make sure we have reminder text
if [[ ${#reminder[0]} -lt 3 ]]; then
error "No reminder text given, addition canceled."
return
fi
reminderEntry="REM "
noDays=1
for ((i=2;i<=8;i++)) ; do
if [[ "${reminder[i]}" != "FALSE" ]]; then
reminderEntry+="${reminder[i]} "
noDays=0
fi
done
if [[ $noDays -eq 1 ]]; then
error "No days were selected for the reminder. Nothing changed."
return
fi
reminderEntry+="AT ${reminder[9]}:${reminder[10]}${reminder[11]} +5 REPEAT weekly MSG ${reminder[0]} %2."
echo "# Added by I38." >> ~/.reminders
echo "$reminderEntry" >> ~/.reminders
if [[ -N ~/.reminders ]]; then
message "Reminder added."
else
error "Something went wrong. The reminder was not added."
fi
}
view_reminders() {
if ! [[ -r ~/.reminders ]]; then
error "No reminders found."
return
fi
mapfile -t lines < ~/.reminders
# Create an empty array to store cleaned-up reminders
yadMenu=()
# Iterate through the reminder lines and clean them up
for i in "${lines[@]}"; do
# Remove the "REM" prefix and leading/trailing spaces
formattedLine="${i#*REM }"
# Remove MSG from the output.
formattedLine="${formattedLine/MSG /}"
# remove the usually %2. from the end of the line, but accept any digit in case someone changes it.
formattedLine="${formattedLine% %[[:digit:]].}"
# Add to the menu
yadMenu+=("$formattedLine")
done
# Display the reminders
reminder="$(yad --list --title "I38 - Reminders" --text "Current reminders:" \
--column "Reminder" "${yadMenu[@]}" \
--button="Close!gtk-ok:1" --button="Delete!gtk-delete:0" --response=1)"
if [[ $? -ne 0 ]]; then
return
fi
if [[ "${reminder:0:1}" == "#" ]]; then
error "Please select the actual reminder to be deleted, anything starting with # is only a comment. Nothing changed."
return
fi
# Remove the | from the end of reminder
reminder="${reminder%|}"
# Find the index to remove from lines.
for i in "${!yadMenu[@]}" ; do
if [[ "${yadMenu[i]}" == "${reminder}" ]]; then
# Delete selected reminder and possible preceeding comment.
commentIndex=$((i - 1))
if [[ "${lines[commentIndex]:0:1}" == "#" ]]; then
unset lines[$commentIndex]
fi
unset lines[$i]
message "Reminder deleted."
printf "%s\n" "${lines[@]}" > ~/.reminders
fi
done
}
view_today() {
if ! [[ -r ~/.reminders ]]; then
error "No reminders found."
return
fi
mapfile -t lines < <(rem | tr -s $'\n')
# Display the reminders
yad --list --title "I38 - Reminders" --text "Today's reminders:" \
--column "Reminder" "${lines[@]}" \
--button="Close!gtk-ok:0"
}
if ! command -v remind &> /dev/null ; then
error "Please install remind. For notifications, please make sure to have notification-daemon and notify-send as well. Run i38.sh to regenerate your i3 config after the needed components are installed."
exit 1
fi
if [[ $# -ne 0 ]]; then
sox -ndqV0 synth .1 tri 600 norm -9 pad .05 repeat
notification="$*"
if [[ "${notification}" =~ "http" ]]; then
url="${notification##*http}"
url="${url%%[[:space:]]*}"
url="http${url}"
notification="${notification/${url}[[:space:]]/}"
url="${url%[[:space:].?!]}"
fi
if command -v xclip &> /dev/null && [[ "${#url}" -gt 3 ]]; then
echo "${url}" | xclip -selection clipboard
notify-send --hint=int:transient:1 -t 500 -r 38 "${notification} The URL has been copied to the clipboard."
else
notify-send --hint=int:transient:1 -t 500 -r 38 "${*}"
fi
exit 0
fi
while : ; do
action=$(yad --title "I38 - Reminders" --form \
--button="_View Today's Reminders!gtk-info":3 \
--button="_View All Reminders!gtk-info":4 \
--button="_Add Reminder!gtk-edit":0 \
--button="_Add Daily Reminder!gtk-edit":2 \
--button="_Add Weekly Reminder!gtk-edit":5 \
--button="Add Monthly Reminder!gtk-edit":6 \
--button="Add Custom Reminder!gtk-edit":7 \
--button="Close!gtk-cancel":1 \
--separator="")
case $? in
0)
# Single reminder
add_reminder
;;
1|252)
# Handle "Close" button click and escape.
exit 0
;;
2)
# Handle "Add Daily Reminder" button click
add_daily_reminder
;;
3)
# View today's reminders
view_today
;;
4)
# View reminders
view_reminders
;;
5)
# Handle "Add Weekly Reminder" button click
add_weekly_reminder
;;
6)
# Handle "Add Monthly Reminder" button click
add_monthly_reminder
;;
7)
# Handle "Add Custom Reminder" button click
add_custom_reminder
;;
esac
done

View File

@ -1,6 +1,16 @@
#!/usr/bin/env bash
# This file is part of I38.
# I38 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 3 of the License, or (at your option) any later version.
# I38 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 I38. If not, see <https://www.gnu.org/licenses/>.
write_history(){
oldHistory="$(grep -v "$txt" "${historyPath}/.history" | head -n 49)"
echo -e "$txt\n$oldHistory" | sed 's/^$//g' > "${historyPath}/.history"

35
scripts/screen_controller.sh Executable file
View File

@ -0,0 +1,35 @@
#!/usr/bin/env bash
# This file is part of I38.
# I38 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 3 of the License, or (at your option) any later version.
# I38 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 I38. If not, see <https://www.gnu.org/licenses/>.
# Not for multiple screens.
# Get the name of the screen.
screenName="$(xrandr --query | grep "connected" | cut -d ' ' -f1 | head -n 1)"
menuOptions=(
"1.0" "Maximum Brightness"
"0.75" "75 percent"
"0.5" "50 percent"
"0.25" "25 percent"
"0" "Screen Curtain"
)
brightness="$(yad --list --title "I38" --text "Set Screen Brightness" --columns 2 --hide-column 1 --column "" --column "Select" "${menuOptions[@]}")"
if [[ ${#brightness} -lt 1 ]]; then
exit 0
fi
xrandr --output ${screenName} --brightness ${brightness%%|*} &&
spd-say -P important -Cw "Screen set to ${brightness#*|}."
exit 0

View File

@ -1,6 +1,15 @@
#!/usr/bin/env python3
# Written by Storm Dragon, Jeremiah, and contributers.
# Released under the terms of the WTFPL http://www.wtfpl.net
# This file is part of I38.
# I38 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 3 of the License, or (at your option) any later version.
# I38 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 I38. If not, see <https://www.gnu.org/licenses/>.
import i3ipc
from i3ipc import Event
@ -10,14 +19,25 @@ i3 = i3ipc.Connection()
def on_new_window(self,i3):
system('play -n synth .25 sin 440:880 sin 480:920 remix - norm -3 pitch -500 &')
if i3.container.name == 'xfce4-notifyd':
system('play -nqV0 synth .05 sq 1800 tri 2400 delay 0 .03 remix - repeat 2 echo .55 0.7 20 1 norm -12 &')
else:
system('play -nqV0 synth .25 sin 440:880 sin 480:920 remix - norm -3 pitch -500 &')
def on_close_window(self,i3):
if i3.container.name != 'xfce4-notifyd':
system('play -nqV0 synth .25 sin 880:440 sin 920:480 remix - norm -3 pitch -500 &')
def on_mode(self,event):
mode= event.change
if mode == 'default':
if mode == 'ratpoison':
system('play -qV0 "|sox -np synth .07 sq 400" "|sox -np synth .5 sq 800" fade h 0 .5 .5 norm -20 &')
elif mode == 'bypass':
system('play -nqV0 synth .1 saw 700 saw 1200 delay 0 .04 remix - norm -6')
elif mode == 'default':
system('play -qV0 "|sox -np synth .07 sq 400" "|sox -np synth .5 sq 800" fade h 0 .5 .5 norm -20 reverse &')
else:
system('play -qV0 "|sox -np synth .07 sq 400" "|sox -np synth .5 sq 800" fade h 0 .5 .5 norm -20 &')
system('play -nqV0 synth 0.05 pluck F3 norm -8 : synth 0.05 pluck C4 norm -8 : synth 0.05 pluck F4 norm -8 : synth 0.05 pluck C5 norm -8')
def on_workspace_focus(self,i3):
#system('play -qnV0 synth pi fade 0 .25 .15 pad 0 1 reverb overdrive riaa norm -8 speed 1 &')
@ -27,17 +47,18 @@ def on_workspace_move(self,i3):
system('play -qnV0 synth pi fade 0 .25 .15 pad 0 1 reverb overdrive riaa norm -8 speed 1 reverse &')
def on_restart(self,i3):
system('play -qn synth .25 saw 500:1200 fade .1 .25 .1 norm -8 &')
system('play -qnV0 synth .25 saw 500:1200 fade .1 .25 .1 norm -8 &')
def on_exit(self,i3):
system('play -qn synth .3 sin 700:200 fade 0 .3 0 &')
system('play -qnV0 synth .3 sin 700:200 fade 0 .3 0 &')
def on_fullscreen(self,i3):
system('play -qn synth br flanger fade h .3 .3 0 &')
system('play -qnV0 synth br flanger fade h .3 .3 0 &')
i3 = i3ipc.Connection()
i3.on('window::new', on_new_window)
i3.on('window::close', on_close_window)
i3.on(Event.MODE, on_mode)
i3.on('workspace::focus', on_workspace_focus)
i3.on('window::move', on_workspace_move)

110
scripts/sysinfo.sh Executable file
View File

@ -0,0 +1,110 @@
#!/usr/bin/env bash
# Initialize variables
cpuUsage=0
cpuTemp=0
memoryUsed=0
memoryTotal=0
memoryPercent=0
swapUsed=0
swapTotal=0
swapPercent=0
diskUsed=0
diskTotal=0
diskPercent=0
networkSent=0
networkRecv=0
# Helper function for temperature conversion
celsius_to_fahrenheit() {
local celsius="$1"
[[ -z "$celsius" || "$celsius" == "--" ]] && echo "--" && return
[[ ! "$celsius" =~ ^-?[0-9]+(\.[0-9]+)?$ ]] && echo "--" && return
local fahrenheit
fahrenheit=$(echo "scale=1; ($celsius * 9/5) + 32" | bc -l)
echo "$fahrenheit"
}
update_system_data() {
# CPU usage
cpuUsage=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}')
cpuUsage=$(printf "%.1f" "$cpuUsage" 2>/dev/null || echo "$cpuUsage")
# CPU temperature - fix for high readings
local tempCelsius="--"
if [[ -f /sys/class/thermal/thermal_zone0/temp ]]; then
tempCelsius=$(cat /sys/class/thermal/thermal_zone0/temp)
# Check if we need to divide by 1000 (common format)
if [[ $tempCelsius -gt 200 ]]; then
tempCelsius=$(echo "scale=1; $tempCelsius/1000" | bc -l)
fi
elif [[ -f /sys/class/hwmon/hwmon0/temp1_input ]]; then
tempCelsius=$(cat /sys/class/hwmon/hwmon0/temp1_input)
if [[ $tempCelsius -gt 200 ]]; then
tempCelsius=$(echo "scale=1; $tempCelsius/1000" | bc -l)
fi
elif command -v sensors &>/dev/null; then
tempCelsius=$(sensors | grep -oP 'Core 0.*?\+\K[0-9.]+')
fi
[[ "$tempCelsius" != "--" && "$tempCelsius" != "null" ]] && cpuTemp=$(celsius_to_fahrenheit "$tempCelsius") || cpuTemp="--"
# Memory usage
memoryTotal=$(free -m | awk '/^Mem:/{print $2/1024}')
memoryUsed=$(free -m | awk '/^Mem:/{print $3/1024}')
memoryPercent=$(free | awk '/^Mem:/{printf("%.1f", $3/$2 * 100)}')
# Swap usage
swapTotal=$(free -m | awk '/^Swap:/{print $2/1024}')
swapUsed=$(free -m | awk '/^Swap:/{print $3/1024}')
[[ "$swapTotal" -gt 0 ]] && swapPercent=$(free | awk '/^Swap:/{printf("%.1f", $3/$2 * 100)}') || swapPercent=0
# Disk usage
diskTotal=$(df -h / | awk 'NR==2 {print $2}')
diskUsed=$(df -h / | awk 'NR==2 {print $3}')
diskPercent=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%')
# Network usage
networkSent=$(cat /proc/net/dev | grep -v "lo:" | awk '{s+=$10} END {printf "%.2f", s/1024/1024}')
networkRecv=$(cat /proc/net/dev | grep -v "lo:" | awk '{r+=$2} END {printf "%.2f", r/1024/1024}')
}
display_system_info() {
update_system_data
# Create the system information text with proper line breaks
systemInfoText="System Information
CPU
Usage: ${cpuUsage}%
Temperature: ${cpuTemp}° F
Memory
Usage: ${memoryUsed} / ${memoryTotal} GB (${memoryPercent}%)
Swap: ${swapUsed} / ${swapTotal} GB (${swapPercent}%)
Disk (Root Partition)
Usage: ${diskUsed} / ${diskTotal} GB (${diskPercent}%)
Network (Total Since Boot)
Received: ${networkRecv} MB
Sent: ${networkSent} MB
End of text. Press Control+Home to return to the beginning."
# Display in text-info dialog for screen reader accessibility
echo "$systemInfoText" | yad --pname=I38System \
--title="I38 System Information" \
--text-info \
--show-cursor \
--width=400 \
--height=400 \
--center \
--button="Close:0"
}
display_system_info
exit 0

104
scripts/toggle_screenreader.sh Executable file
View File

@ -0,0 +1,104 @@
#!/usr/bin/env bash
# This file is part of I38.
#
# I38 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 3 of the License, or (at your option) any later version.
#
# I38 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 I38. If not, see <https://www.gnu.org/licenses/>.
is_running() {
pgrep -x "$1" >/dev/null
return $?
}
speak() {
spd-say -P important -Cw -- "$*"
}
# Make sure screen readers are available
orcaAvailable=false
cthulhuAvailable=false
if command -v orca &> /dev/null; then
orcaAvailable=true
fi
if command -v cthulhu &> /dev/null; then
cthulhuAvailable=true
fi
# Check if at least one screen reader is available
if ! $orcaAvailable && ! $cthulhuAvailable; then
speak "No screen readers found. Please install Orca or Cthulhu."
yad --center --title="I38" --alert="No screen readers found.\nPlease install Orca or Cthulhu."
exit 1
fi
# Determine current state
currentReader="none"
if is_running "cthulhu"; then
currentReader="cthulhu"
elif is_running "orca"; then
currentReader="orca"
fi
# Build YAD command based on available screen readers
items=()
# First add the currently active screen reader
if [ "$currentReader" != "none" ]; then
if [ "$currentReader" = "cthulhu" ] && $cthulhuAvailable; then
items+=("Cthulhu (active)" "cthulhu")
elif [ "$currentReader" = "orca" ] && $orcaAvailable; then
items+=("Orca (active)" "orca")
fi
fi
if $cthulhuAvailable && [ "$currentReader" != "cthulhu" ]; then
items+=("Cthulhu" "cthulhu")
fi
if $orcaAvailable && [ "$currentReader" != "orca" ]; then
items+=("Orca" "orca")
fi
# Display the dialog
result=$(yad --center --title="Screen Reader Toggle" --text="Select screen reader:" \
--list --on-top --skip-taskbar \
--button="Cancel:1" --button="OK:0" \
--column="Screen Reader" --column=ID:HD "${items[@]}")
exitCode=$?
if [ $exitCode -ne 0 ]; then
exit 0
fi
if [ -n "$result" ]; then
# Extract the selected reader from the result
selectedReader=$(echo "$result" | cut -d'|' -f2)
# Don't do anything if selecting the already active reader
if [ "$selectedReader" = "$currentReader" ]; then
exit 0
fi
# Stop current screen reader
if [ "$currentReader" != "none" ]; then
speak "Switching from $currentReader to $selectedReader."
pkill -15 "$currentReader"
sleep 0.5
else
speak "Starting $selectedReader."
fi
if [ "$selectedReader" = "orca" ]; then
orca &
else
cthulhu &
fi
fi
exit 0

439
scripts/weather.sh Executable file
View File

@ -0,0 +1,439 @@
#!/usr/bin/env bash
# Configuration settings
defaultCity="Raleigh, NC"
defaultLat="35.78"
defaultLon="-78.64"
tempUnit="F"
updateInterval=30
configDir="${XDG_CONFIG_HOME:-$HOME/.config}/stormux/I38"
configFile="$configDir/weather.conf"
mkdir -p "$configDir"
# Initialize variables
cityName="Detecting..."
latitude=0.0
longitude=0.0
currentTemp="--"
currentHumidity="--"
currentWindSpeed="--"
currentWindSpeedMph="--"
currentConditions="Unknown"
weatherLastUpdate=0
severeWeatherAlerted=0
declare -a forecastDates=("--" "--" "--")
declare -a forecastFormattedDates=("--" "--" "--")
declare -a forecastMinTemps=("--" "--" "--")
declare -a forecastMaxTemps=("--" "--" "--")
declare -a forecastConditions=("Unknown" "Unknown" "Unknown")
declare -A weatherCodes
weatherCodes[0]="Clear sky"
weatherCodes[1]="Mainly clear"
weatherCodes[2]="Partly cloudy"
weatherCodes[3]="Overcast"
weatherCodes[45]="Fog"
weatherCodes[48]="Rime fog"
weatherCodes[51]="Light drizzle"
weatherCodes[53]="Moderate drizzle"
weatherCodes[55]="Dense drizzle"
weatherCodes[56]="Light freezing drizzle"
weatherCodes[57]="Dense freezing drizzle"
weatherCodes[61]="Slight rain"
weatherCodes[63]="Moderate rain"
weatherCodes[65]="Heavy rain"
weatherCodes[66]="Light freezing rain"
weatherCodes[67]="Heavy freezing rain"
weatherCodes[71]="Slight snow fall"
weatherCodes[73]="Moderate snow fall"
weatherCodes[75]="Heavy snow fall"
weatherCodes[77]="Snow flurries"
weatherCodes[80]="Slight rain showers"
weatherCodes[81]="Moderate rain showers"
weatherCodes[82]="Heavy rain showers"
weatherCodes[85]="Slight snow showers"
weatherCodes[86]="Heavy snow showers"
weatherCodes[95]="Thunderstorm"
weatherCodes[96]="Thunderstorm with slight hail"
weatherCodes[99]="Thunderstorm with heavy hail"
declare -a severeWeatherCodes=(65 67 75 82 86 95 96 99)
# Button return codes
refreshBtn=0
quitBtn=1
settingsBtn=2
trap "pkill -P $$" EXIT INT TERM
# Load configuration if available
if [ -f "$configFile" ]; then
source "$configFile"
# Convert lastWeatherUpdate string to integer if it exists
[[ -n "$lastWeatherUpdate" ]] && weatherLastUpdate=$lastWeatherUpdate || weatherLastUpdate=0
if [[ -n "$city" ]]; then
cityName="$city"
latitude="$latitude"
longitude="$longitude"
fi
# Try to reload saved weather data
if [[ "$weatherLastUpdate" -gt 0 && "$currentTemp" == "--" ]]; then
[[ -n "$savedCurrentTemp" ]] && currentTemp="$savedCurrentTemp"
[[ -n "$savedCurrentHumidity" ]] && currentHumidity="$savedCurrentHumidity"
[[ -n "$savedCurrentConditions" ]] && currentConditions="$savedCurrentConditions"
[[ -n "$savedCurrentWindSpeed" ]] && currentWindSpeedMph="$savedCurrentWindSpeed"
for i in {0..2}; do
varDate="savedForecastDate_$i"
varMin="savedForecastMin_$i"
varMax="savedForecastMax_$i"
varCond="savedForecastCond_$i"
[[ -n "${!varDate}" ]] && forecastFormattedDates[$i]="${!varDate}"
[[ -n "${!varMin}" ]] && forecastMinTemps[$i]="${!varMin}"
[[ -n "${!varMax}" ]] && forecastMaxTemps[$i]="${!varMax}"
[[ -n "${!varCond}" ]] && forecastConditions[$i]="${!varCond}"
done
fi
fi
# Helper functions
time_diff() {
local timestamp="$1"
local now=$(date +%s)
local diff=$((now - timestamp))
if [ $diff -lt 60 ]; then
echo "just now"
elif [ $diff -lt 3600 ]; then
local minutes=$((diff / 60))
echo "$minutes minute$([ $minutes -ne 1 ] && echo "s") ago"
elif [ $diff -lt 86400 ]; then
local hours=$((diff / 3600))
echo "$hours hour$([ $hours -ne 1 ] && echo "s") ago"
else
local days=$((diff / 86400))
echo "$days day$([ $days -ne 1 ] && echo "s") ago"
fi
}
celsius_to_fahrenheit() {
local celsius="$1"
[[ -z "$celsius" || "$celsius" == "--" ]] && echo "--" && return
[[ ! "$celsius" =~ ^-?[0-9]+(\.[0-9]+)?$ ]] && echo "--" && return
local fahrenheit
fahrenheit=$(echo "scale=1; ($celsius * 9/5) + 32" | bc -l)
echo "$fahrenheit"
}
kmh_to_mph() {
local kmh="$1"
[[ -z "$kmh" || "$kmh" == "--" ]] && echo "--" && return
[[ ! "$kmh" =~ ^-?[0-9]+(\.[0-9]+)?$ ]] && echo "--" && return
local mph
mph=$(echo "scale=1; $kmh * 0.621371" | bc -l)
echo "$mph"
}
format_date() {
local isoDate="$1"
[[ -z "$isoDate" || "$isoDate" == "--" ]] && echo "--" && return
date -d "$isoDate" "+%A, %B %d" 2>/dev/null || echo "$isoDate"
}
# Save configuration
save_config() {
cat > "$configFile" << EOF
city="$cityName"
latitude="$latitude"
longitude="$longitude"
tempUnit=$tempUnit
updateInterval=$updateInterval
lastWeatherUpdate=$weatherLastUpdate
savedCurrentTemp="$currentTemp"
savedCurrentHumidity="$currentHumidity"
savedCurrentConditions="$currentConditions"
savedCurrentWindSpeed="$currentWindSpeedMph"
savedForecastDate_0="${forecastFormattedDates[0]}"
savedForecastMin_0="${forecastMinTemps[0]}"
savedForecastMax_0="${forecastMaxTemps[0]}"
savedForecastCond_0="${forecastConditions[0]}"
savedForecastDate_1="${forecastFormattedDates[1]}"
savedForecastMin_1="${forecastMinTemps[1]}"
savedForecastMax_1="${forecastMaxTemps[1]}"
savedForecastCond_1="${forecastConditions[1]}"
savedForecastDate_2="${forecastFormattedDates[2]}"
savedForecastMin_2="${forecastMinTemps[2]}"
savedForecastMax_2="${forecastMaxTemps[2]}"
savedForecastCond_2="${forecastConditions[2]}"
EOF
}
# Play severe weather alert sound using Sox
play_severe_weather_alert() {
if command -v play &>/dev/null; then
# Generate alert sound pattern using sox
play -nqV0 synth 2 sine 853 sine 960 remix - norm -15 &
fi
# Also display notification if available
if command -v notify-send &>/dev/null; then
notify-send "Severe Weather Alert" "Severe weather conditions detected for $cityName: $currentConditions" -u critical
fi
}
# Function to check if a value is in array
in_array() {
local value="$1"
shift
local array=("$@")
for item in "${array[@]}"; do
if [[ "$item" == "$value" ]]; then
return 0 # True, found in array
fi
done
return 1 # False, not found
}
# Function to detect location
get_location() {
# Only try location detection if we don't already have a city name
if [[ "$cityName" == "Detecting..." ]]; then
echo "Attempting to detect location via ipinfo.io..."
# Try to fetch location data
local locationData
locationData=$(curl -s --connect-timeout 5 "https://ipinfo.io/json" 2>/dev/null)
if [[ $? -eq 0 && -n "$locationData" && $(echo "$locationData" | jq -e '.city') ]]; then
echo "Location data received successfully"
cityName=$(echo "$locationData" | jq -r '.city // "Unknown"')
local region=$(echo "$locationData" | jq -r '.region // ""')
# Add region/state to city name if available
[[ -n "$region" ]] && cityName="$cityName, $region"
# Extract coordinates directly from the "loc" field
local loc=$(echo "$locationData" | jq -r '.loc // "0,0"')
latitude=$(echo "$loc" | cut -d',' -f1)
longitude=$(echo "$loc" | cut -d',' -f2)
save_config
else
cityName="$defaultCity"
latitude="$defaultLat"
longitude="$defaultLon"
save_config
fi
fi
}
# Function to fetch weather data
fetch_weather_data() {
local now=$(date +%s)
local elapsedMinutes=$(( (now - weatherLastUpdate) / 60 ))
# Only fetch if needed
if [[ $weatherLastUpdate -eq 0 || $elapsedMinutes -ge $updateInterval ]]; then
local url="https://api.open-meteo.com/v1/forecast?latitude=$latitude&longitude=$longitude&current=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m&daily=weather_code,temperature_2m_max,temperature_2m_min&timezone=auto"
local response=$(curl -s --connect-timeout 10 "$url" 2>/dev/null)
if [[ $? -eq 0 && -n "$response" && $(echo "$response" | jq -e '.current' 2>/dev/null) ]]; then
# Update current weather data
local tempCelsius=$(echo "$response" | jq -r '.current.temperature_2m // "--"' 2>/dev/null)
[[ "$tempCelsius" != "--" && "$tempCelsius" != "null" ]] && currentTemp=$(celsius_to_fahrenheit "$tempCelsius") || currentTemp="--"
currentHumidity=$(echo "$response" | jq -r '.current.relative_humidity_2m // "--"' 2>/dev/null)
[[ "$currentHumidity" == "null" ]] && currentHumidity="--"
currentWindSpeed=$(echo "$response" | jq -r '.current.wind_speed_10m // "--"' 2>/dev/null)
if [[ "$currentWindSpeed" != "--" && "$currentWindSpeed" != "null" ]]; then
currentWindSpeedMph=$(kmh_to_mph "$currentWindSpeed")
else
currentWindSpeed="--"
currentWindSpeedMph="--"
fi
local weatherCode=$(echo "$response" | jq -r '.current.weather_code // 0' 2>/dev/null)
[[ "$weatherCode" == "null" ]] && weatherCode=0
currentConditions="${weatherCodes[$weatherCode]:-Unknown}"
# Check for severe weather and play alert if needed
if in_array "$weatherCode" "${severeWeatherCodes[@]}"; then
if [ "$severeWeatherAlerted" -eq 0 ]; then
play_severe_weather_alert
severeWeatherAlerted=1
fi
else
# Reset alert flag if weather is no longer severe
severeWeatherAlerted=0
fi
# Process forecast data (limited to 3 days)
if [[ $(echo "$response" | jq -e '.daily' 2>/dev/null) ]]; then
for i in {0..2}; do
# Process forecast data
forecastDates[$i]=$(echo "$response" | jq -r ".daily.time[$i] // \"--\"" 2>/dev/null)
[[ "${forecastDates[$i]}" != "--" && "${forecastDates[$i]}" != "null" ]] && \
forecastFormattedDates[$i]=$(format_date "${forecastDates[$i]}") || forecastFormattedDates[$i]="--"
local minTempC=$(echo "$response" | jq -r ".daily.temperature_2m_min[$i] // \"--\"" 2>/dev/null)
[[ "$minTempC" != "--" && "$minTempC" != "null" ]] && \
forecastMinTemps[$i]=$(celsius_to_fahrenheit "$minTempC") || forecastMinTemps[$i]="--"
local maxTempC=$(echo "$response" | jq -r ".daily.temperature_2m_max[$i] // \"--\"" 2>/dev/null)
[[ "$maxTempC" != "--" && "$maxTempC" != "null" ]] && \
forecastMaxTemps[$i]=$(celsius_to_fahrenheit "$maxTempC") || forecastMaxTemps[$i]="--"
local code=$(echo "$response" | jq -r ".daily.weather_code[$i] // 0" 2>/dev/null)
[[ "$code" == "null" ]] && code=0
forecastConditions[$i]="${weatherCodes[$code]:-Unknown}"
done
fi
# Update timestamp
weatherLastUpdate=$(date +%s)
save_config
else
echo "Failed to fetch weather data. Response code: $?"
if [[ -n "$response" ]]; then
echo "First 100 chars of response: ${response:0:100}"
fi
fi
fi
}
# Function to change location (for settings)
change_location() {
local newLocation="$1"
if [[ -n "$newLocation" && "$newLocation" != "$cityName" ]]; then
# Try to parse the location using curl to a geocoding service
local result=$(curl -s --connect-timeout 10 "https://nominatim.openstreetmap.org/search?q=$newLocation&format=json" 2>/dev/null)
if [[ -n "$result" && $(echo "$result" | jq -e '.[0]') ]]; then
cityName="$newLocation"
latitude=$(echo "$result" | jq -r '.[0].lat // "0.0"')
longitude=$(echo "$result" | jq -r '.[0].lon // "0.0"')
# Force weather update
weatherLastUpdate=0
save_config
return 0
else
yad --title "Location Error" --text="Could not find location: $newLocation" --button=gtk-ok
return 1
fi
fi
return 1
}
# Display weather information in a text-info dialog
display_weather() {
local lastUpdateText="Never updated"
[[ "$weatherLastUpdate" -gt 0 ]] && lastUpdateText="Last updated: $(time_diff "$weatherLastUpdate")"
# Create the weather information text with proper line breaks
weatherInfoText="Weather for $cityName
$lastUpdateText
Current Conditions
Temperature: ${currentTemp}° F
Conditions: $currentConditions
Humidity: ${currentHumidity}%
Wind Speed: ${currentWindSpeedMph} mph
3-Day Forecast
────────────────────────────────────
${forecastFormattedDates[0]}
Temp: ${forecastMinTemps[0]}° to ${forecastMaxTemps[0]}° F
Conditions: ${forecastConditions[0]}
────────────────────────────────────
${forecastFormattedDates[1]}
Temp: ${forecastMinTemps[1]}° to ${forecastMaxTemps[1]}° F
Conditions: ${forecastConditions[1]}
────────────────────────────────────
${forecastFormattedDates[2]}
Temp: ${forecastMinTemps[2]}° to ${forecastMaxTemps[2]}° F
Conditions: ${forecastConditions[2]}
End of text. Press Control+Home to return to the beginning."
# Display in text-info dialog for screen reader accessibility
echo "$weatherInfoText" | yad --pname=I38Weather \
--title="I38 Weather Monitor" \
--text-info \
--show-cursor \
--width=500 \
--height=600 \
--center \
--button="Settings:$settingsBtn" \
--button="Refresh:$refreshBtn" \
--button="Close:$quitBtn"
return $?
}
# Function to display settings dialog
display_settings() {
local ret=$(yad --pname=I38WeatherSettings \
--title="I38 Weather Settings" \
--form \
--width=400 \
--center \
--field="Location:":TEXT "$cityName" \
--field="Current Coordinates:":LBL "Lat: $latitude, Lon: $longitude" \
--field="Temperature Unit:":CB "F!C" \
--field="Update Interval (minutes):":NUM "$updateInterval!5..120!5" \
--button="Cancel:1" \
--button="Save:0")
local saveResult=$?
if [[ $saveResult -eq 0 && -n "$ret" ]]; then
local newLocation=$(echo "$ret" | cut -d"|" -f1)
local newUnit=$(echo "$ret" | cut -d"|" -f3)
local newInterval=$(echo "$ret" | cut -d"|" -f4)
# Apply any changes
[[ -n "$newLocation" && "$newLocation" != "$cityName" ]] && change_location "$newLocation"
[[ -n "$newUnit" && "$newUnit" != "$tempUnit" ]] && tempUnit="$newUnit" && save_config
[[ -n "$newInterval" && "$newInterval" != "$updateInterval" ]] && updateInterval="$newInterval" && save_config
fi
}
# Main loop
while : ; do
get_location
fetch_weather_data
# Display weather using the text-info widget
display_weather
ret=$?
# Handle button actions
case $ret in
$refreshBtn)
# Force a weather update
weatherLastUpdate=0
continue
;;
$settingsBtn)
# Display settings dialog
display_settings
continue
;;
$quitBtn|252)
# Quit button or window closed
break
;;
esac
done
exit 0

View File

@ -1,5 +1,20 @@
#!/usr/bin/env bash
# This file is part of I38.
# I38 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 3 of the License, or (at your option) any later version.
# I38 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 I38. If not, see <https://www.gnu.org/licenses/>.
path="$(readlink -f $0)"
path="${path%/*/*}"
path="${path##*/}"
if [[ "$path" == "i3" ]]; then
mapfile -t windowList < <(python3 -c '
import i3ipc
@ -13,4 +28,28 @@ id="$(yad --title "I38" --list --separator "" --column "id" --column "Select Win
if [[ -z "${id}" ]]; then
exit 0
fi
i3-msg \[id="${id}"\] focus
i3-msg \[id="${id}"\] focus
else
mapfile -t windowList < <(python3 -c '
import i3ipc
i3 = i3ipc.Connection()
for con in i3.get_tree():
if con.window or con.type == "con":
if con.name:
print(con.window)
print(con.name)')
# Remove the first entry if it is "none"
if [[ "${windowList[0]}" == "none" ]]; then
unset "windowList[0]"
fi
id="$(yad --title "I38" --list --separator "" --column "id" --column "Select Window" --hide-column 1 --print-column 1 "${windowList[@]}")"
if [[ -z "${id}" ]]; then
exit 0
fi
swaymsg \[id="${id}"\] focus
fi

444
scripts/wormhole.py Executable file
View File

@ -0,0 +1,444 @@
#!/usr/bin/env python3
# This file is part of I38.
# I38 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 3 of the License, or (at your option) any later version.
# I38 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 I38. If not, see <https://www.gnu.org/licenses/>.
import gi
import os
import subprocess
import threading
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk, GLib, Pango
DEFAULT_DOWNLOAD_DIR = os.path.expanduser("~/Downloads")
class WormholeGUI(Gtk.Window):
def __init__(self):
super().__init__(title="Magic Wormhole GUI")
self.set_border_width(10)
self.set_default_size(500, 400)
self.download_dir = DEFAULT_DOWNLOAD_DIR
self.notebook = Gtk.Notebook()
self.add(self.notebook)
self.init_main_tab()
self.init_settings_tab()
# Escape key closes app
self.connect("key-press-event", self.on_key_press)
def init_main_tab(self):
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
self.notebook.append_page(main_box, Gtk.Label(label="Main"))
button_box = Gtk.Box(spacing=10)
self.send_button = Gtk.Button(label="Send")
self.send_button.connect("clicked", self.on_send_clicked)
button_box.pack_start(self.send_button, True, True, 0)
self.receive_button = Gtk.Button(label="Receive")
self.receive_button.connect("clicked", self.on_receive_clicked)
button_box.pack_start(self.receive_button, True, True, 0)
main_box.pack_start(button_box, False, False, 0)
# Add a frame for the code display
code_frame = Gtk.Frame(label="Wormhole Code")
main_box.pack_start(code_frame, False, False, 5)
code_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
code_box.set_border_width(5)
code_frame.add(code_box)
self.code_display = Gtk.Entry()
self.code_display.set_editable(False)
code_box.pack_start(self.code_display, False, False, 0)
# Add a frame for progress output
progress_frame = Gtk.Frame(label="Transfer Progress")
main_box.pack_start(progress_frame, True, True, 5)
# Add a scrolled window for the progress text
scrolled_window = Gtk.ScrolledWindow()
scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
progress_frame.add(scrolled_window)
# Add a text view for progress output
self.progress_text = Gtk.TextView()
self.progress_text.set_editable(False)
self.progress_text.set_cursor_visible(False)
self.progress_text.set_wrap_mode(Gtk.WrapMode.WORD)
self.progress_text.override_font(Pango.FontDescription("Monospace 10"))
self.progress_buffer = self.progress_text.get_buffer()
scrolled_window.add(self.progress_text)
# Add action buttons
action_box = Gtk.Box(spacing=10)
self.copy_button = Gtk.Button(label="Copy Code")
self.copy_button.connect("clicked", self.copy_code)
action_box.pack_start(self.copy_button, True, True, 0)
self.cancel_button = Gtk.Button(label="Cancel")
self.cancel_button.connect("clicked", self.cancel_transfer)
action_box.pack_start(self.cancel_button, True, True, 0)
main_box.pack_start(action_box, False, False, 0)
def init_settings_tab(self):
settings_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
settings_box.set_border_width(10)
self.notebook.append_page(settings_box, Gtk.Label(label="Settings"))
# Create a frame for download settings
download_frame = Gtk.Frame(label="Download Location")
settings_box.pack_start(download_frame, False, False, 0)
# Add a container for the frame content
download_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
download_box.set_border_width(10)
download_frame.add(download_box)
# Add a description label
description = Gtk.Label(label="Files will be saved to this directory:")
description.set_xalign(0) # Align to left
download_box.pack_start(description, False, False, 0)
# Add a directory selector
dir_box = Gtk.Box(spacing=5)
download_box.pack_start(dir_box, False, False, 5)
# Add an entry to show the current path
self.dir_entry = Gtk.Entry()
self.dir_entry.set_text(self.download_dir)
dir_box.pack_start(self.dir_entry, True, True, 0)
# Add a browse button
browse_button = Gtk.Button(label="Browse...")
browse_button.connect("clicked", self.on_browse_clicked)
dir_box.pack_start(browse_button, False, False, 0)
# Add a save button
save_button = Gtk.Button(label="Save Settings")
save_button.connect("clicked", self.on_save_settings)
download_box.pack_start(save_button, False, False, 5)
def on_key_press(self, widget, event):
if event.keyval == Gdk.KEY_Escape:
# Check if a transfer is currently in progress
if hasattr(self, 'current_process') and self.current_process and self.current_process.poll() is None:
# Show a dialog indicating transfer is in progress
dialog = Gtk.MessageDialog(
parent=self,
flags=0,
message_type=Gtk.MessageType.WARNING,
buttons=Gtk.ButtonsType.OK,
text="Transfer in Progress"
)
dialog.format_secondary_text("Please wait for the transfer to complete or cancel it before closing.")
dialog.run()
dialog.destroy()
return True
else:
# No transfer in progress, confirm quit
dialog = Gtk.MessageDialog(
parent=self,
flags=0,
message_type=Gtk.MessageType.QUESTION,
buttons=Gtk.ButtonsType.YES_NO,
text="Quit Application"
)
dialog.format_secondary_text("Are you sure you want to quit?")
response = dialog.run()
dialog.destroy()
if response == Gtk.ResponseType.YES:
Gtk.main_quit()
return True
return False
def on_browse_clicked(self, button):
"""Browse for a directory"""
dialog = Gtk.FileChooserDialog(
title="Select Download Directory",
parent=self,
action=Gtk.FileChooserAction.SELECT_FOLDER,
buttons=(
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OPEN, Gtk.ResponseType.OK
)
)
# Set the current folder to the current download directory
if os.path.exists(self.download_dir):
dialog.set_current_folder(self.download_dir)
if dialog.run() == Gtk.ResponseType.OK:
self.dir_entry.set_text(dialog.get_filename())
dialog.destroy()
def on_save_settings(self, button):
"""Save the settings"""
new_dir = self.dir_entry.get_text().strip()
# Handle ~ in path
if new_dir.startswith("~"):
new_dir = os.path.expanduser(new_dir)
# Validate the directory
if not os.path.isdir(new_dir):
try:
os.makedirs(new_dir, exist_ok=True)
except Exception as e:
self.show_error(f"Could not create directory: {e}")
return
# Update the directory
self.download_dir = new_dir
# Show confirmation
self.show_info("Settings saved successfully.")
def on_download_dir_changed(self, widget):
self.download_dir = widget.get_filename()
def on_send_clicked(self, widget):
chooser = Gtk.FileChooserDialog(
title="Select File or Folder", parent=self,
action=Gtk.FileChooserAction.OPEN,
buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
)
chooser.set_select_multiple(False)
chooser.set_local_only(True)
chooser.set_modal(True)
chooser.set_property("show-hidden", False)
chooser.connect("key-press-event", self.on_key_press)
if chooser.run() == Gtk.ResponseType.OK:
path = chooser.get_filename()
chooser.destroy()
self.send_file(path)
else:
chooser.destroy()
def send_file(self, path):
self.code_display.set_text("Sending...")
self.clear_progress()
self.update_progress(f"Starting to send: {os.path.basename(path)}\n")
# Initialize current_process attribute
self.current_process = None
def send():
self.current_process = subprocess.Popen(
["wormhole", "send", path],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
stdin=subprocess.DEVNULL,
text=True
)
for line in self.current_process.stdout:
print("SEND OUTPUT:", line.strip()) # Debug info
# Update the progress display
GLib.idle_add(self.update_progress, line)
if "Wormhole code is:" in line:
code = line.strip().split(":", 1)[-1].strip()
GLib.idle_add(self.code_display.set_text, code)
self.current_process.stdout.close()
self.current_process.wait()
# Clear the current_process when done
GLib.idle_add(self.clear_current_process)
threading.Thread(target=send, daemon=True).start()
def on_receive_clicked(self, widget):
dialog = Gtk.Dialog(
title="Enter Wormhole Code",
parent=self,
flags=0
)
dialog.add_buttons(
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OK, Gtk.ResponseType.OK
)
dialog.connect("key-press-event", self.on_key_press)
# Make OK button the default
ok_button = dialog.get_widget_for_response(Gtk.ResponseType.OK)
ok_button.set_can_default(True)
ok_button.grab_default()
entry = Gtk.Entry()
entry.set_activates_default(True)
entry.grab_focus()
box = dialog.get_content_area()
box.set_border_width(10)
box.set_spacing(10)
box.add(Gtk.Label(label="Enter the wormhole code:"))
box.add(entry)
box.show_all()
response = dialog.run()
if response == Gtk.ResponseType.OK:
code = entry.get_text()
dialog.destroy()
self.receive_file(code)
else:
dialog.destroy()
def receive_file(self, code):
self.code_display.set_text("Receiving...")
self.clear_progress()
self.update_progress(f"Starting to receive with code: {code}\n")
# Initialize current_process attribute
self.current_process = None
def receive():
# Save current directory
original_dir = os.getcwd()
try:
# Create download directory if it doesn't exist
os.makedirs(self.download_dir, exist_ok=True)
# Change to download directory before starting the process
os.chdir(self.download_dir)
self.update_progress(f"Downloading to: {self.download_dir}\n")
# Start the wormhole receive process
self.current_process = subprocess.Popen(
["wormhole", "receive", "--accept-file"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True
)
# Send the code to the process
self.current_process.stdin.write(code + "\n")
self.current_process.stdin.flush()
self.current_process.stdin.close()
for line in self.current_process.stdout:
print("RECEIVE OUTPUT:", line.strip()) # Debug info
# Update the progress display
GLib.idle_add(self.update_progress, line)
# Handle questions about accepting the file
if "ok? (y/N):" in line:
# Auto-accept the file
self.current_process.stdin = open("/dev/stdin", "w")
self.current_process.stdin.write("y\n")
self.current_process.stdin.flush()
self.current_process.stdin.close()
if "Received file" in line or "File received" in line:
GLib.idle_add(self.code_display.set_text, "File received.")
self.current_process.stdout.close()
self.current_process.wait()
# Add final status message
GLib.idle_add(self.update_progress, f"\nFile saved to: {self.download_dir}\n")
except Exception as e:
GLib.idle_add(self.update_progress, f"Error: {e}\n")
finally:
# Change back to original directory
os.chdir(original_dir)
# Clear the current_process when done
GLib.idle_add(self.clear_current_process)
threading.Thread(target=receive, daemon=True).start()
def copy_code(self, widget):
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
clipboard.set_text(self.code_display.get_text(), -1)
self.update_progress("Code copied to clipboard.\n")
def cancel_transfer(self, widget):
if hasattr(self, 'current_process') and self.current_process and self.current_process.poll() is None:
try:
self.current_process.terminate()
self.update_progress("Transfer process terminated.\n")
except Exception as e:
self.update_progress(f"Error canceling transfer: {e}\n")
self.code_display.set_text("Transfer canceled.")
self.update_progress("Transfer canceled by user.\n")
self.clear_current_process()
def update_progress(self, text):
"""Update the progress text view"""
end = self.progress_buffer.get_end_iter()
self.progress_buffer.insert(end, text)
# Scroll to the end
self.progress_text.scroll_to_iter(end, 0.0, False, 0.0, 0.0)
return False # Required for GLib.idle_add
def clear_progress(self):
"""Clear the progress text view"""
self.progress_buffer.set_text("")
def show_error(self, message):
"""Show an error dialog"""
dialog = Gtk.MessageDialog(
parent=self,
flags=0,
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.OK,
text="Error"
)
dialog.format_secondary_text(message)
dialog.run()
dialog.destroy()
def show_info(self, message):
"""Show an info dialog"""
dialog = Gtk.MessageDialog(
parent=self,
flags=0,
message_type=Gtk.MessageType.INFO,
buttons=Gtk.ButtonsType.OK,
text="Information"
)
dialog.format_secondary_text(message)
dialog.run()
dialog.destroy()
def clear_current_process(self):
"""Clear the current process reference"""
self.current_process = None
return False # Required for GLib.idle_add
def main():
app = WormholeGUI()
app.connect("destroy", Gtk.main_quit)
app.show_all()
Gtk.main()
if __name__ == "__main__":
main()